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

作者: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. 每天学习30分钟新知识之html教程1

    版本 年份 HTML 1991 HTML+ 1993 HTML 2.0 1995 HTML 3.2 1997 HTML 4.01 1999 XHTML 1.0 2000 HTML5 2012 XHTM ...

  2. jq bootstrap select 点击不能动弹

    jq  bootstrap select 点击不能动弹   因为是样式selectpicker  冲突. 解决办法换 样式  form-control <select name="ty ...

  3. linux 上拷贝文件到windows 上 文件出现锁的文件

    要在linux上拷贝出文件到windows上,那么文件必须是777的最高权限. chmod wb_redis -R

  4. 嵌入式驱动开发之sensor---"VIP0 PortA", "VIP0 PortB", "VIP1 PortA", "VIP1 PortB",dvo0(vout1) dvo1(vout0)

    (1)vip 简介 (2)vip 电路图 (3)vip 更换采集相机输入 (4)vip 驱动 ---------------------author:pkf --------------------- ...

  5. Spring Cloud 微服务二:API网关spring cloud zuul

    前言:本章将继续上一章Spring Cloud微服务,本章主要内容是API 网关,相关代码将延续上一章,如需了解请参考:Spring Cloud 微服务一:Consul注册中心 Spring clou ...

  6. ASP.NET动态网站制作(22)-- ADO.NET(1)

    前言:这节课开始真正地学习WEB开发,ADO.NET就是一组允许.NET开发人员使用标准的.机构化的,甚至无连接的方式与数据交互的技术.所属的类库为:System.Data.dll. 内容: 1.AD ...

  7. iOS开发 Xcode8 问题

      一.证书管理 用Xcode8打开工程后,比较明显的就是下图了,这个是苹果的新特性,可以帮助我们自动管理证书.建议大家勾选这个Automatically manage signing(Ps.但是在b ...

  8. Android源码下载之----内核需要另外下载

    用repo sync下载的android源码默认不包含kernel目录,需要自己另外下载. 下载命令:$ git clone https://android.googlesource.com/kern ...

  9. 【智力题】IO——行测、笔试、面试中遇到的

    昨天(05.23)下午去参加了明源软件的暑期实习宣讲+笔试,第一次听说这个行业,行业和笔试风格完全不一样啊,5道行测智力题+1个问答+ 斐波那契数列 + 洗牌算法(思想.流程图.代码),今年回来后线上 ...

  10. [转]springmvc常用注解标签详解

    1.@Controller 在SpringMVC 中,控制器Controller 负责处理由DispatcherServlet 分发的请求,它把用户请求的数据经过业务处理层处理之后封装成一个Model ...