我一直在研究 .Net 4.0 中一些新的并行功能的实用性。
假设我有这样的代码:
foreach (var item in myEnumerable)
myDatabase.Insert(item.ConvertToDatabase());
想象 myDatabase.Insert 正在执行一些工作以插入到 SQL 数据库。
理论上你可以写:
Parallel.ForEach(myEnumerable, item => myDatabase.Insert(item.ConvertToDatabase()));
您会自动获得利用多核优势的代码。
但是如果 myEnumerable 只能由单线程交互怎么办? Parallel 类会通过单个线程进行枚举并仅将结果分派给循环中的工作线程吗?
如果 myDatabase 只能通过单线程交互怎么办?在循环的每次迭代中建立一个数据库连接肯定不会更好。
最后,如果我的“var item”恰好是 UserControl 或者必须在 UI 线程上交互的东西怎么办?
我应该遵循什么设计模式来解决这些问题?
在我看来,当您处理实际应用程序时,切换到 Parallel/PLinq/etc 并不容易。
IEnumerable<T>
接口本质上不是线程安全的。 Parallel.ForEach
将自动处理此问题,并且仅并行化枚举中的项目。 (该序列将始终按顺序遍历,一次一个元素 - 但生成的对象会并行化。)
如果您的类(即:T)不能由多个线程处理,那么您不应该尝试并行化此例程。并非每个序列都适合并行化 - 这就是编译器不自动完成并行化的原因之一;)
如果您正在做需要使用 UI 线程的工作,这仍然是可能的。但是,您需要像在后台线程上处理用户界面元素时一样小心,并将数据封送回 UI 线程。在许多情况下,使用新的
TaskScheduler.FromCurrentSynchronizationContext
API 可以简化这一过程。我在我的博客上写了这个场景。
所有这些都是合理的问题 - PLINQ/TPL 不会尝试解决它们。 作为开发人员,编写在并行时可以正常运行的代码仍然是您的工作。编译器/TPL/PLINQ 无法将多线程不安全的代码转换为线程安全的代码……您必须确保这样做。
对于您描述的某些情况,您应该首先决定并行化是否合理。如果瓶颈是获取与数据库的连接或确保正确的操作顺序,那么多线程可能不合适。
在 TPL 如何将可枚举流式传输到多个线程的情况下,您的假设是正确的。 在单个线程上枚举序列,然后(可能)将每个工作项分派到要执行操作的单独线程。
IEnumerable<T>
接口本质上是不是线程安全,但 TPL 在幕后为您处理此问题.
PLINQ/TPL 帮助您做的是管理何时以及如何将工作分派给多个线程。TPL 会检测计算机上何时存在多个内核,并自动调整用于处理数据的线程数。如果一台机器只有一个 CPU/核心,那么 TPL 可能会选择“不并行化”工作。作为开发人员,您的好处是不必编写两条不同的路径 - 一条用于并行逻辑,一条用于顺序逻辑。但是,您仍然有责任确保可以从多个线程同时安全地访问您的代码。
我应该遵循什么设计模式 解决这些问题?这个问题没有一个答案……但是,一般做法是在对象设计中采用“不变性”。不变性使得跨多个线程使用对象变得更安全,并且是使操作可并行化的最常见实践之一。事实上,像 F# 这样的语言广泛地利用了不变性,使该语言能够帮助简化并发编程。
如果您使用的是 .NET 4.0,您还应该查看 ConcurrentXXX
中的
System.Collections.Concurrent
集合类。在这里您会发现一些无锁和细粒度的锁定集合构造,使编写多线程代码变得更容易。
正如您所猜测的,利用 Parallel.For
Parallel.ForEach
要求您有能力将您的工作组合成可以独立执行的离散单元(由传递给
Parallel.ForEach
的 lambda 语句体现) .
这里的答案和评论中有一个很好的讨论:Parallel.For():更新循环外的变量。不:并行扩展不会为你思考。多线程问题在这里仍然是现实的。这是很好的语法糖,但不是万能药。
这是一个非常好的问题,答案并不是100%清晰/简洁。我会向您指出 Microsoft 的这篇参考资料,它详细介绍了何时应使用并行项目
。