温故之.NET 任务并行

文章讲述了.NET中的任务并行,通过Parallel.Invoke创建并行任务,强调了Task.Run在UI线程中的使用以及线程与Task的区别。同时提出了并行编程的建议,包括避免过度并行化、使用Partitioner和注意线程安全问题。
摘要由CSDN通过智能技术生成

这篇文章主要讲解 .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() 到底谁先执行至此,这篇文章的内容讲解完毕。

后话最近看了一些书籍,决定无论何时,凡是关注了我的朋友,都一律关注回去源于以下一点:尊重是相互的,学习也是相互的

网络安全成长路线图

这个方向初期比较容易入门一些,掌握一些基本技术,拿起各种现成的工具就可以开黑了。不过,要想从脚本小子变成hei客大神,这个方向越往后,需要学习和掌握的东西就会越来越多,以下是学习网络安全需要走的方向:

# 网络安全学习方法

​ 上面介绍了技术分类和学习路线,这里来谈一下学习方法:
​ ## 视频学习

​ 无论你是去B站或者是油管上面都有很多网络安全的相关视频可以学习,当然如果你还不知道选择那套学习,我这里也整理了一套和上述成长路线图挂钩的视频教程,完整版的视频已经上传至CSDN官方,朋友们如果需要可以点击这个链接免费领取。网络安全重磅福利:入门&进阶全套282G学习资源包免费分享!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值