一直觉得自己对并发了解不够深入,特别是看了《代码整洁之道》觉得自己有必要好好学学并发编程,因为性能也是衡量代码整洁的一大标准。而且在《失控》这本书中也多次提到并发,不管是计算机还是生物都并发处理着各种事物。人真是奇怪,当你关注一个事情的时候,你会发现周围的事物中就常出现那个事情。所以好奇心驱使下学习并发。便有了此文。

作者:Stoneniqiu来源:博客园|2015-10-13 09:18

Tech Neo技术沙龙 | 11月25号,九州云/ZStack与您一起探讨云时代网络边界管理实践

一直觉得自己对并发了解不够深入,特别是看了《代码整洁之道》觉得自己有必要好好学学并发编程,因为性能也是衡量代码整洁的一大标准。而且在《失控》这本书中也多次提到并发,不管是计算机还是生物都并发处理着各种事物。人真是奇怪,当你关注一个事情的时候,你会发现周围的事物中就常出现那个事情。所以好奇心驱使下学习并发。便有了此文。

一、理解硬件线程和软件线程

多核处理器带有一个以上的物理内核--物理内核是真正的独立处理单元,多个物理内核使得多条指令能够同时并行运行。硬件线程也称为逻辑内核,一个物理内 核可以使用超线程技术提供多个硬件线程。所以一个硬件线程并不代表一个物理内核;Windows中每个运行的程序都是一个进程,每一个进程都会创建并运行 一个或多个线程,这些线程称为软件线程。硬件线程就像是一条泳道,而软件线程就是在其中游泳的人。

二、并行场合

.Net Framework4 引入了新的Task Parallel Library(任务并行库,TPL),它支持数据并行、任务并行和流水线。让开发人员应付不同的并行场合。

  • 数据并行:有大量数据需要处理,并且必须对每一份数据执行同样的操作。比如通过256bit的密钥对100个Unicode字符串进行AES算法加密。

  • 任务并行:通过任务并发运行不同的操作。例如生成文件散列码,加密字符串,创建缩略图。

  • 流水线:这是任务并行和数据并行的结合体。

TPL引入了System.Threading.Tasks ,主类是Task,这个类表示一个异步的并发的操作,然而我们不一定要使用Task类的实例,可以使用Parallel静态类。它提供了 Parallel.Invoke, Parallel.For Parallel.Forecah 三个方法。

三、Parallel.Invoke

试图让很多方法并行运行的最简单的方法就是使用Parallel类的Invoke方法。例如有四个方法:

  • WatchMovie

  • HaveDinner

  • ReadBook

  • WriteBlog

通过下面的代码就可以使用并行。

System.Threading.Tasks.Parallel.Invoke(WatchMovie, HaveDinner, ReadBook, WriteBlog);

这段代码会创建指向每一个方法的委托。Invoke方法接受一个Action的参数组。

1

public static void Invoke(params Action[] actions);

用lambda表达式或匿名委托可以达到同样的效果。

System.Threading.Tasks.Parallel.Invoke(() => WatchMovie(), () => HaveDinner(), () => ReadBook(), delegate() { WriteBlog(); });

 1.没有特定的执行顺序。

Parallel.Invoke方法只有在4个方法全部完成之后才会返回。它至少需要4个硬件线程才足以让这4个方法并发运行。但并不保证这4个方法能够同时启动运行,如果一个或者多个内核处于繁忙状态,那么底层的调度逻辑可能会延迟某些方法的初始化执行。

给方法加上延时,就可以看到必须等待最长的方法执行完成才回到主方法。

  1. static void Main(string[] args)
  2. {
  3. System.Threading.Tasks.Parallel.Invoke(WatchMovie, HaveDinner, ReadBook,
  4. WriteBlog);
  5. Console.WriteLine("执行完成");
  6. Console.ReadKey();
  7. }
  8. static void WatchMovie()
  9. {
  10. Thread.Sleep(5000);
  11. Console.WriteLine("看电影");
  12. }
  13. static void HaveDinner()
  14. {
  15. Thread.Sleep(1000);
  16. Console.WriteLine("吃晚饭");
  17. }
  18. static void ReadBook()
  19. {
  20. Thread.Sleep(2000);
  21. Console.WriteLine("读书");
  22. }
  23. static void WriteBlog()
  24. {
  25. Thread.Sleep(3000);
  26. Console.WriteLine("写博客");
  27. }

这样会造成很多逻辑内核处于长时间闲置状态。

四、Parallel.For

Parallel.For为固定数目的独立For循环迭代提供了负载均衡 (即将工作分发到不同的任务中执行,这样所有的任务在大部分时间都可以保持繁忙) 的并行执行。从而能尽可能地充分利用所有的可用的内核。

我们比较下下面两个方法,一个使用For循环,一个使用Parallel.For  都是生成密钥在转换为十六进制字符串。

private static void GenerateAESKeys() 
        { 
            var sw = Stopwatch.StartNew(); 
            for (int i = 0; i < NUM_AES_KEYS; i++) 
            { 
                var aesM = new AesManaged(); 
                aesM.GenerateKey(); 
                byte[] result = aesM.Key; 
                string hexStr = ConverToHexString(result); 
            } 
            Console.WriteLine("AES:"+sw.Elapsed.ToString()); 
        } 
 
 private static void ParallelGenerateAESKeys() 
        { 
            var sw = Stopwatch.StartNew(); 
            System.Threading.Tasks.Parallel.For(1, NUM_AES_KEYS + 1, (int i) => 
            { 
                var aesM = new AesManaged(); 
                aesM.GenerateKey(); 
                byte[] result = aesM.Key; 
                string hexStr = ConverToHexString(result); 
            }); 
 
            Console.WriteLine("Parallel_AES:" + sw.Elapsed.ToString()); 
        } 

private static int NUM_AES_KEYS = 100000;
        static void Main(string[] args)
        {
            Console.WriteLine("执行"+NUM_AES_KEYS+"次:"); GenerateAESKeys();
            ParallelGenerateAESKeys();
            Console.ReadKey();
        }

执行1000000次

这里并行的时间是串行的一半。

五、Parallel.ForEach

在Parallel.For中,有时候对既有循环进行优化可能会是一个非常复杂的任务。Parallel.ForEach为固定数目的独立For Each循环迭代提供了负载均衡的并行执行,且支持自定义分区器,让使用者可以完全掌握数据分发。实质就是将所有要处理的数据区分为多个部分,然后并行运 行这些串行循环。

修改上面的代码:

  1. System.Threading.Tasks.Parallel.ForEach(Partitioner.Create(1, NUM_AES_KEYS + 1), range =>
  2. {
  3. var aesM = new AesManaged();
  4. Console.WriteLine("AES Range({0},{1} 循环开始时间:{2})",range.Item1,range.Item2,DateTime.Now.TimeOfDay);
  5. for (int i = range.Item1; i < range.Item2; i++)
  6. {
  7. aesM.GenerateKey();
  8. byte[] result = aesM.Key;
  9. string hexStr = ConverToHexString(result);
  10. }
  11. Console.WriteLine("AES:"+sw.Elapsed.ToString());
  12. });

从执行结果可以看出,分了13个段执行的。

第二次执行还是13个段。速度上稍微有差异。开始没有指定分区数,Partitioner.Create使用的是内置默认值。

而且我们发现这些分区并不是同时执行的,大致是分了三个时间段执行。而且执行顺序是不同的。总的时间和Parallel.For的方法差不多。

public static ParallelLoopResult ForEach<TSource>(Partitioner<TSource> source, Action<TSource> body)

Parallel.ForEach方法定义了source和Body两个参数。source是指分区器。提供了分解为多个分区的数据源。body是 要调用的委托。它接受每一个已定义的分区作为参数。一共有20多个重载,在上面的例子中,分区的类型为Tuple<int,int>,是一个 二元组类型。此外,返回一个ParallelLoopResult的值。

Partitioner.Create 创建分区是根据逻辑内核数及其他因素决定。

  1. public static OrderablePartitioner<Tuple<int, int>> Create(int fromInclusive, int toExclusive)
  2. {
  3. int num = 3;
  4. if (toExclusive <= fromInclusive)
  5. throw new ArgumentOutOfRangeException("toExclusive");
  6. int rangeSize = (toExclusive - fromInclusive) / (PlatformHelper.ProcessorCount * num);
  7. if (rangeSize == 0)
  8. rangeSize = 1;
  9. return Partitioner.Create<Tuple<int, int>>(Partitioner.CreateRanges(fromInclusive, toExclusive, rangeSize), EnumerablePartitionerOptions.NoBuffering);
  10. }

因此我们可以修改分区数目,rangesize大致为250000左右。也就是说我的逻辑内核是4.

var rangesize = (int) (NUM_AES_KEYS/Environment.ProcessorCount) + 1;
   System.Threading.Tasks.Parallel.ForEach(Partitioner.Create(1, NUM_AES_KEYS + 1,rangesize), range =>

再次执行:

分区变成了四个,时间上没有多大差别(第一个时间是串行时间)。我们看见这四个分区几乎是同时执行的。大部分情况下,TPL在幕后使用的负载均衡机制都是非常高效的,然而对分区的控制便于使用者对自己的工作负载进行分析,来改进整体的性能。

Parallel.ForEach也能对IEnumerable<int>集合进行重构。Enumerable.Range生产了序列化的数目。但这样就没有上面的分区效果。

  1. private static void ParallelForEachGenerateMD5HasHes()
  2. {
  3. var sw = Stopwatch.StartNew();
  4. System.Threading.Tasks.Parallel.ForEach(Enumerable.Range(1, NUM_AES_KEYS), number =>
  5. {
  6. var md5M = MD5.Create();
  7. byte[] data = Encoding.Unicode.GetBytes(Environment.UserName + number);
  8. byte[] result = md5M.ComputeHash(data);
  9. string hexString = ConverToHexString(result);
  10. });
  11. Console.WriteLine("MD5:"+sw.Elapsed.ToString());
  12. }

六、从循环中退出

和串行运行中的break不同,ParallelLoopState 提供了两个方法用于停止Parallel.For 和 Parallel.ForEach的执行。

  • Break:让循环在执行了当前迭代后尽快停止执行。比如执行到100了,那么循环会处理掉所有小于100的迭代。

  • Stop:让循环尽快停止执行。如果执行到了100的迭代,那不能保证处理完所有小于100的迭代。

修改上面的方法:执行3秒后退出。

  1. private static void ParallelLoopResult(ParallelLoopResult loopResult)
  2. {
  3. string text;
  4. if (loopResult.IsCompleted)
  5. {
  6. text = "循环完成";
  7. }
  8. else
  9. {
  10. if (loopResult.LowestBreakIteration.HasValue)
  11. {
  12. text = "Break终止";
  13. }
  14. else
  15. {
  16. text = "Stop 终止";
  17. }
  18. }
  19. Console.WriteLine(text);
  20. }
  21. private static void ParallelForEachGenerateMD5HasHesBreak()
  22. {
  23. var sw = Stopwatch.StartNew();
  24. var loopresult= System.Threading.Tasks.Parallel.ForEach(Enumerable.Range(1, NUM_AES_KEYS), (int number,ParallelLoopState loopState) =>
  25. {
  26. var md5M = MD5.Create();
  27. byte[] data = Encoding.Unicode.GetBytes(Environment.UserName + number);
  28. byte[] result = md5M.ComputeHash(data);
  29. string hexString = ConverToHexString(result);
  30. if (sw.Elapsed.Seconds > 3)
  31. {
  32. loopState.Stop();
  33. }
  34. });
  35. ParallelLoopResult(loopresult);
  36. Console.WriteLine("MD5:" + sw.Elapsed);
  37. }

七、捕捉并行循环中发生的异常。

当并行迭代中调用的委托抛出异常,这个异常没有在委托中被捕获到时,就会变成一组异常,新的System.AggregateException负责处理这一组异常。

  1. private static void ParallelForEachGenerateMD5HasHesException()
  2. {
  3. var sw = Stopwatch.StartNew();
  4. var loopresult = new ParallelLoopResult();
  5. try
  6. {
  7. loopresult = System.Threading.Tasks.Parallel.ForEach(Enumerable.Range(1, NUM_AES_KEYS), (number, loopState) =>
  8. {
  9. var md5M = MD5.Create();
  10. byte[] data = Encoding.Unicode.GetBytes(Environment.UserName + number);
  11. byte[] result = md5M.ComputeHash(data);
  12. string hexString = ConverToHexString(result);
  13. if (sw.Elapsed.Seconds > 3)
  14. {
  15. throw new TimeoutException("执行超过三秒");
  16. }
  17. });
  18. }
  19. catch (AggregateException ex)
  20. {
  21. foreach (var innerEx in  ex.InnerExceptions)
  22. {
  23. Console.WriteLine(innerEx.ToString());
  24. }
  25. }
  26. ParallelLoopResult(loopresult);
  27. Console.WriteLine("MD5:" + sw.Elapsed);
  28. }

结果:

异常出现了好几次。

 八、指定并行度。

TPL的方法总会试图利用所有可用的逻辑内核来实现最好的结果,但有时候你并不希望在并行循环中使用所有的内核。比如你需要留出一个不参与并行计算 的内核,来创建能够响应用户的应用程序,而且这个内核需要帮助你运行代码中的其他部分。这个时候一种好的解决方法就是指定最大并行度。

这需要创建一个ParallelOptions的实例,设置MaxDegreeOfParallelism的值。

  1. private static void ParallelMaxDegree(int maxDegree)
  2. {
  3. var parallelOptions = new ParallelOptions();
  4. parallelOptions.MaxDegreeOfParallelism = maxDegree;
  5. var sw = Stopwatch.StartNew();
  6. System.Threading.Tasks.Parallel.For(1, NUM_AES_KEYS + 1, parallelOptions, (int i) =>
  7. {
  8. var aesM = new AesManaged();
  9. aesM.GenerateKey();
  10. byte[] result = aesM.Key;
  11. string hexStr = ConverToHexString(result);
  12. });
  13. Console.WriteLine("AES:" + sw.Elapsed.ToString());
  14. }

调用:如果在四核微处理器上运行,那么将使用3个内核。

ParallelMaxDegree(Environment.ProcessorCount - 1);

时间上大致慢了点(第一次Parallel.For 3.18s),但可以腾出一个内核来处理其他的事情。

小结:这次学习了Parallel相关方法以及如何退出并行循环和捕获异常、设置并行度,还有并行相关的知识。园子里也有类似的博客。但作为自己知识的管理,在这里梳理一遍。

详细的.Net并行编程高级教程--Parallel的更多相关文章

  1. Net并行编程高级教程--Parallel

    Net并行编程高级教程--Parallel 一直觉得自己对并发了解不够深入,特别是看了<代码整洁之道>觉得自己有必要好好学学并发编程,因为性能也是衡量代码整洁的一大标准.而且在<失控 ...

  2. 【读书笔记】.Net并行编程高级教程--Parallel

    一直觉得自己对并发了解不够深入,特别是看了<代码整洁之道>觉得自己有必要好好学学并发编程,因为性能也是衡量代码整洁的一大标准.而且在<失控>这本书中也多次提到并发,不管是计算机 ...

  3. 【读书笔记】.Net并行编程高级教程(二)-- 任务并行

    前面一篇提到例子都是数据并行,但这并不是并行化的唯一形式,在.Net4之前,必须要创建多个线程或者线程池来利用多核技术.现在只需要使用新的Task实例就可以通过更简单的代码解决命令式任务并行问题. 1 ...

  4. .Net并行编程高级教程(二)-- 任务并行

    前面一篇提到例子都是数据并行,但这并不是并行化的唯一形式,在.Net4之前,必须要创建多个线程或者线程池来利用多核技术.现在只需要使用新的Task实例就可以通过更简单的代码解决命令式任务并行问题. 1 ...

  5. 《C#并行编程高级教程》第9章 异步编程模型 笔记

    这个章节我个人感觉意义不大,使用现有的APM(异步编程模型)和EAP(基于时间的异步模型)就很够用了,针对WPF和WinForm其实还有一些专门用于UI更新的类. 但是出于完整性,还是将一下怎么使用. ...

  6. 《C#并行编程高级教程》第2章 命令式编程 笔记

    Parallel.Invoke 并行执行多个方法,只有在所有方法都执行后才会返回 static void Main(string[] args){    Parallel.Invoke(    () ...

  7. 《C#并行编程高级教程》第8章 线程池 笔记

    主要的几个概念(详细最好还是看书,配合插图看)   任务是会被分配到线程上的,而这些线程都在线程池引擎下管理 线程池引擎管理着合适数量的线程池,线程从全局队列获取工作项执行. .NET4 Framew ...

  8. 《C#并行编程高级教程》第7章 VS2010任务调试 笔记

    没有什么好说的,主要是将调试模式下的Parallel Tasks窗体和Parallel Stacks窗体.折腾一下应该比看书效果好.(表示自己没有折腾过) 另外值得注意的是,主线程不是一个任务.所以主 ...

  9. 《C#并行编程高级教程》第6章 PLINQ:声明式数据并行 笔记

    PLINQ这个话题好多书都写到过,这本也没有什么特别好的地方. 几个有用和有趣的点记录一下.   顺序的不确定性 用PLINQ就一定要记住并行后会导致顺序不确定的问题.解决方案就是AsOrdered或 ...

随机推荐

  1. (2) yum源配置-163

    1.获取yum源文件 登录http://mirrors.163.com/.help/centos.html,查看CentOS6的链接地址(右键点击“CentOS6”,选择复制链接地址),链接地址为:h ...

  2. Unable to VNC onto Centos server remotely

    用的好好的vncserver 突然遇到这个错误: [vnc@localhost ~]$ sudo systemctl status vncserver@:1.service -l● vncserver ...

  3. unity回调函数范例

    using System.Collections; using System.Collections.Generic; using UnityEngine; public class callback ...

  4. 1、Codevs 必做:2833、1002、1003、2627、2599

    2833 奇怪的梦境  时间限制: 1 s  空间限制: 128000 KB  题目等级 : 黄金 Gold 题解       题目描述 Description Aiden陷入了一个奇怪的梦境:他被困 ...

  5. (比赛)B - Super Mobile Charger

    B - Super Mobile Charger Time Limit:1000MS     Memory Limit:32768KB     64bit IO Format:%I64d & ...

  6. Java中线程和线程池

    Java中开启多线程的三种方式 1.通过继承Thread实现 public class ThreadDemo extends Thread{ public void run(){ System.out ...

  7. ABAP服务器文件操作

    转自http://blog.itpub.net/547380/viewspace-876667/ 在程序设计开发过程中,很多要对文件进行操作,这又分为对本地文件操作和服务器文件操作.对本地文件操作使用 ...

  8. Shell中的while循环

    while循环的格式   while expression do command command ``` done 1.计数器控制的while循环    主要用于已经准确知道要输入的数据和字符串的数目 ...

  9. wap网站即手机端网页SEO优化注意事项及方法

    定位和页面设计: 无论是PC端还是移动端,网站 都要考虑清楚消费群体的定位问题.虽然智能手机用户数量非常普及,但是要明白中国的大部分手机用户使用的还是2G网络,一直高 喊的3G.4G手机用户只有大约1 ...

  10. 关于python2中的unicode和str以及python3中的str和bytes

    python3有两种表示字符序列的类型:bytes和str.前者的实例包含原始的8位值:后者的实例包含Unicode字符. python2中也有两种表示字符序列的类型,分别叫做str和unicode. ...