【问题标题】:Parallel features in .Net 4.0.Net 4.0 中的并行功能
【发布时间】:2010-05-05 18:26:56
【问题描述】:

我一直在讨论 .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 并不容易。

【问题讨论】:

    标签: c# .net multithreading parallel-processing


    【解决方案1】:

    IEnumerable<T> 接口本质上不是线程安全的。 Parallel.ForEach 将自动处理此问题,并且仅并行化来自您的枚举的项目。 (将始终按顺序遍历序列,一次一个元素 - 但生成的对象会被并行化。)

    如果您的类(即:T)不能由多个线程处理,那么您不应该尝试并行化此例程。并非每个序列都是并行化的候选者——这也是编译器不能自动完成并行化的原因之一;)

    如果您正在做需要使用 UI 线程的工作,这仍然是可能的。但是,您需要像在后台线程上处理用户界面元素并将数据编组回 UI 线程时一样小心。在许多情况下,这可以使用新的TaskScheduler.FromCurrentSynchronizationContext API 来简化。我写了this scenario on my blog here

    【讨论】:

    • 迄今为止的最佳答案,但附带问题:假设我的循环体执行长时间运行的 IO 操作(网络请求、数据库等),Parallel 类是否会检测到睡眠/暂停线程并自动启动一个新的?还是会受限于机器上的核心数量?
    • @Jonathan.Peppers:默认的任务调度程序可以很好地处理这个问题。它将为这种情况注入额外的工作。 (默认情况下,ThreadPool 使用的元素比线程多得多,并且会根据工作负载动态缩减)
    【解决方案2】:

    所有这些都是合法的问题 - PLINQ/TPL 不会尝试解决这些问题。 作为开发人员,编写在并行化时可以正常运行的代码仍然是您的工作。编译器/TPL/PLINQ 无法将多线程不安全的代码转换为线程安全的代码……您必须确保这样做。

    对于您描述的某些情况,您应该首先确定并行化是否明智。如果瓶颈将是获取与数据库的连接或确保正确的操作顺序,那么也许多线程不是不合适。

    在 TPL 如何将枚举流传输到多个线程的情况下,您的假设是正确的。 在单个线程上枚举序列,然后(可能)将每个工作项分派到单独的线程以执行操作。IEnumerable<T> 接口本质上是不是线程安全的,但 TPL 会在幕后为您处理。

    PLINQ/TPL 可以帮助您管理何时以及如何将工作分派给多个线程。 TPL 检测机器上何时有多个内核并自动调整使用的线程数来处理数据。如果一台机器只有一个 CPU/Core,那么 TPL 可能会选择不并行化工作。开发人员的好处是不必编写两种不同的路径 - 一种用于并行逻辑,一种用于顺序逻辑。但是,您仍然有责任确保您的代码可以同时从多个线程安全地访问。

    我应该遵循什么设计模式 解决这些问题?

    这个问题没有一个答案...但是,一般做法是在您的对象设计中使用immutability。不变性使跨多个线程使用对象变得更安全,并且是使操作可并行化的最常见做法之一。事实上,像 F# 这样的语言广泛利用了不变性,以使该语言能够帮助简化并发编程。

    如果您使用的是 .NET 4.0,您还应该查看 System.Collections.Concurrent 中的 ConcurrentXXX 集合类。在这里,您会发现一些无锁和细粒度的锁定集合结构,它们使编写多线程代码更加容易。

    【讨论】:

      【解决方案3】:

      正如您所推测的,利用Parallel.ForParallel.ForEach 需要您能够将您的工作组合成可以执行的离散单元(由传递给Parallel.ForEach 的lambda 语句体现) 独立。

      【讨论】:

      • 现实世界中的问题是否符合此标准?换句话说,普通应用程序甚至能够使用这些并行功能吗?
      • @Jonathan:当然。看看 Scott Hanselman 的这个演示文稿,他展示了一个生动的例子来说明这是如何工作的。 channel9.msdn.com/posts/matthijs/… 演示从演讲开始 38 分 55 秒开始,到 47:02 结束。
      • 显然他们的网站在跳到 38:55 时遇到了一些问题,我将不得不在家观看整件事并回复您。我仍然怀疑他们是否会提供一个很好的例子。
      • 好吧,终于加载了。在我看来,他的 Parallel.For/ForEach 示例看起来可以更好地由 RDMS 处理。使用 Task 类和取消令牌等其他类似乎完全合法。所以我唯一的抱怨是 Parallel.For/ForEach 的可用性。这对于实现特定算法等完全有用,但不是普通的现实世界应用程序。
      【解决方案4】:

      这里有很多关于答案和 cmets 的讨论:Parallel.For(): Update variable outside of loop

      答案是:并行扩展不会为您考虑。多线程问题在这里仍然存在。这是很好的语法糖,但不是灵丹妙药。

      【讨论】:

      • 它不仅仅是语法糖。例如,您可以指定并行度,并连接一个取消例程,该例程将优雅地展开所有线程。
      【解决方案5】:

      这是一个非常好的问题,但答案不是 100% 清晰/简洁。我会向您指出来自 Micrsoft 的这篇参考资料,它提供了关于 WHEN you should use the parallel items 的很多细节。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-02-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-12-02
        • 1970-01-01
        相关资源
        最近更新 更多