温故之.NET 任务并行

这篇文章主要讲解 .NET 的任务并行,与数据并行不一样的是:数据并行以数据为处理单元,而任务并行,则以任务(工做)为单元安全

关于任务的理解,若是还有疑问,能够参考以前的文章【温故之.NET 异步】微信

任务并行基础

若是咱们想要建立并行的任务,能够经过 Parallel.Invoke 来实现。它能够很方便的帮助咱们同时运行多个任务,以下异步

public static void WorkOne() {
    // 任务一
}
public static void WorkTwo() {
    // 任务二
}
Parallel.Invoke(WorkOne, WorkTwo);

// 咱们也能够经过 Lambda 表达式这样写
Parallel.Invoke(
    () => {
        // 任务一
    }, () => {
        // 任务二
    }
);
复制代码

借助 Parallel.Invoke ,咱们只需表达想同时运行的操做,CLR 会处理全部线程调度的具体信息(包括将线程数量自动缩放至计算机上的内核数)性能

须要特别注意
TPL 在后台建立的 Task 数量不必定与所提供的操做的数量相等。 由于 TPL 可能会针对操做的数量进行不一样程度的优化学习

所以,对 Parallel.Invoke ,咱们能够这样理解(只是为了理解方便,不表示其内部具体实现也是这样的)优化

  • 分配一个具备 4 个线程的“线程池”(假设计算机处理器为 4 核 4 线程)。或者根据指定的 ParallelOptions 中的 MaxDegreeOfParallelism 属性来肯定具体数量
  • 采用 Task.Run 的方式运行每个任务。每执行一个任务,就从“线程池”中取一个空闲的线程。若是没有多余的空闲线程,则等待
  • 直处处理完全部的任务为止

这也能够理解为对其内部实现的一个猜想。若是有兴趣,可使用 .NET Refactor 看一下其源码spa

若是程序有 UI 线程,且任务的建立从 UI 线程开始,那么在使用方式上会有变化,以下代码所示线程

Task.Run(() => {
    Parallel.Invoke(
    () => {
        // 任务一
    }, () => {
        // 任务二
    });
});
复制代码

这对于其余的并行(如数据并行)也是同样的。
只要咱们须要从 UI 线程建立并行,就应该使用 Task.Run 来启动它们。不然,颇有可能产生死锁(通常出如今当并行代码内部须要访问 UI 的状况下,其余状况我也暂时没有遇到过)设计

若是咱们分不清当前建立并行的是 UI 线程仍是其余类型的线程。咱们能够统一使用 Task 的方式来启动它们。反正在大部分状况下,使用 Task 来启动也不会形成什么性能问题code

不过,若是咱们须要并行当即启动,或者尽快启动,使用 Task 来启动可能就不太合适,在系统工做量比较重的状况下,咱们也不清楚这个 Task 何时可以执行。
在这种场景下,咱们能够新建一个 Thread 来作这件事。由于 ThreadTask 不一样,Thread 不以任务为单位,当咱们调用 Thread.Start() 的时候,线程就会当即执行。而 Task,当咱们调用 Task.Run 的时候,它须要接受 TPL 的调度(Task Scheduler)。所以,其执行时间就不肯定了

针对建立并行,有如下建议

  • 在不肯定建立并行的是 UI 线程仍是其余线程时,使用 Task.Run 来启动并行(如前面例子所示)
  • 在系统工做比较重的状况下,若是但愿并行可以当即启动,咱们应该使用 Thread 的方式
  • 不然,在大多数状况下,不管 PC 端、Web 端、仍是 WebApi 后台,咱们使用 Task.Run 来启动并行是比较好的方式

经过 Thread 方式启动并行,示例以下

Thread thread = new Thread(() => {
    Parallel.Invoke(
    () => {
        Debug.WriteLine("Work 1");
    },() => {
        Debug.WriteLine("Work 2");
    });
});
thread.Start();
复制代码

针对并行的建议

前面提到,在多处理器条件下,使用 Parallel 能够显著提高性能。但事物总有两面性,所以仍是有一些坑须要咱们注意

  • 对于任务并行,若是任务间具备强关联性(即有不少任务的执行依赖于其余的任务或者多个任务之间存在资源共享)。我的不建议使用并行库,由于在以往的经验中,这样的处理并无为咱们带来特别大的性能提高
  • Parallel.ForParallel.ForEach 以数据并行为主;Parallel.Invoke 以任务并行为主
  • 不要对循环进行过分并行化。所谓物极必反,过分的并行化,不但增长了管理的难度,线程间的同步以及最后各个分区的合并,都会对性能形成影响
  • 若是并行里面的单次迭代的工做量较小,推荐使用 Partitioner 来手动的对源集合进行分块
  • 避免在并行代码块内调用非线程安全的方法,就算是声明为线程安全的方法,也应该尽可能少的调用
  • 尽可能避免在 UI 线程上执行并行循环。也应尽可能避免在并行代码中更新 UI,由于这有可能会产生数据损坏或死锁
  • 在并行迭代中,咱们不该该假定每个迭代顺序开始。好比有集合 [1,2,3,4,5,6,7,8],假设分为 4 个分块 [1,2]、[3,4]、[5,6]、[7,8],咱们不该该认为 [1,2] 这个块要比 [5,6] 这个块先执行。理解这个很重要,能够防止咱们写出可能产生死锁的代码,示例如【示例A】所示

示例A

ManualResetEventSlim mre = new ManualResetEventSlim();
int processor = Environment.ProcessorCount;
var source = Enumerable.Range(0, processor * 100);
Parallel.ForEach(source, item => {
    if (item == processor) {
        mre.Set();
    } else {
        mre.Wait();
    }
});
复制代码

对于这段代码,就可能会(可能性很是大)发生死锁。如前面【针对并行的建议】的最后一点所说,一样地,此处咱们也没法肯定 mre.Set()mre.Wait() 到底谁先执行


至此,这篇文章的内容讲解完毕。

后话
最近看了一些书籍,决定不管什么时候,凡是关注了个人朋友,都一概关注回去
源于如下一点:尊重是相互的,学习也是相互的

在此,也感谢在微信公众号、知乎、简书、掘金等内容平台关注个人朋友。欢迎关注公众号【嘿嘿的学习日记】,全部的文章,都会在公众号首发,Thank you~

公众号二维码
相关文章
相关标签/搜索