博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
温故之.NET 任务并行
阅读量:6201 次
发布时间:2019-06-21

本文共 2932 字,大约阅读时间需要 9 分钟。

这篇文章主要讲解 .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 看一下其源码

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

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

这对于其他的并行(如数据并行)也是一样的。

只要我们需要从 UI 线程创建并行,就应该使用 Task.Run 来启动它们。否则,很有可能产生死锁(一般出现在当并行代码内部需要访问 UI 的情况下,其他情况我也暂时没有遇到过)

如果我们分不清当前创建并行的是 UI 线程还是其他类型的线程。我们可以统一使用 Task 的方式来启动它们。反正在大部分情况下,使用 Task 来启动也不会造成什么性能问题

不过,如果我们需要并行立即启动,或者尽快启动,使用 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~

转载于:https://juejin.im/post/5b36ee16f265da59a23f32d1

你可能感兴趣的文章
wget参数用法详解
查看>>
使用curl命令查看访问url的时间
查看>>
WinForm中跨线程操作控件
查看>>
CODING 敏捷实践完全指南
查看>>
unittest测试框架和测试报告的输出实例(一)
查看>>
下MFC中对象、句柄、ID之间的区别.
查看>>
如何构建Win32汇编的编程环境(ONEPROBLEM个人推荐)
查看>>
Asp.Net MVC 分页、检索、排序整体实现
查看>>
Flymeos插桩适配教程
查看>>
还在用PS磨皮去皱?看看如何用神经网络高度还原你的年轻容貌!
查看>>
大端模式与小端模式、网络字节顺序与主机字节顺序
查看>>
微信支付申请90%的商户都卡在这儿了,申请微信支付,商户功能设置详细说明...
查看>>
制作一款微信表情
查看>>
高仿Instagram 页面效果android特效
查看>>
我的友情链接
查看>>
Juniper 基于路由的×××
查看>>
HDU - 2018 - 母牛的故事(dp)
查看>>
基于matlab的fft变换中参数的设置
查看>>
如何查找JSP页面中的错误
查看>>
2016 年总结
查看>>