自VS2010发布近半年了,虽然整天想学习新东西,要更新到自己时,发现原来自己基本也很懒,2008还没用上多久呢,无奈被2010了。用了几天,IDE模样还是和05、08差不多,加了些小特性,以后慢慢体验吧,第一感觉启动速度慢多了。主要还是.Net 4.0的变化,其实也就是修修补补,语言特性几乎没什么新特性,C#多了个Dynamic,十年前VB就支持的晚绑定。只好把注意力放在了Framework上,新加的并行支持应该是最大的变化吧。

  VS2010发布会我也去过的,并行支持是一大卖点。当时记得台上一个MM对一个Linq查询语句加了个AsParallel(),性能就神奇地提高了一倍,台下掌声雷动。确实不费吹灰之力提高程序性能,是最能引起大家兴趣的。在看电子期刊时,看到冷冷同学,还有吴秦的文章,给偶这些菜鸟以震撼的启发,原来偶已经远远落在了在读大学生的后面。

  那就开始学吧,就拿Parallel开刀。先抓个垫背的:

view source print?
1 static void Set(int length)
2         {
3             var array = new int[length, length, length];
4             for (int i = 0; i < length; i++)
5                 for (int j = 0; j < length; j++)
6                     for (int k = 0; k < length; k++)
7                         array[i, j, k] = System.Threading.Thread.CurrentThread.ManagedThreadId;
8         }

再请出真神:

view source print?
01 static void ParallelSet(int length)
02        {
03            var array = new int[length, length, length];
04            Parallel.For(0, length, i =>
05            {
06                for (int j = 0; j < length; j++)
07                    for (int k = 0; k < length; k++)
08                        array[i, j, k] = System.Threading.Thread.CurrentThread.ManagedThreadId;
09            });
10        }

PK:

view source print?
1 CodeTimer.Time("Single thread", 100, () => Set(100));
2 CodeTimer.Time("Multiple thread", 100, () => ParallelSet(100));

结果,1136ms:729ms,果然不错。不过MSDN的例子说不定是被和谐过的,所以偶总会变变试验过程。果然发现另有乾坤。

<!-- 文章title -->

.Net 4.0并行库实用性演练

<!-- 作者信息等 -->

作者: XiaoMing  发布时间: 2010-09-13 17:53  阅读: 413 次  原文链接   全屏阅读  [收藏]  

<!-- 页码和简介 -->

编辑点评:文中作者深入浅出的介绍了.Net4.0的并行编程,利用.Net 4.0的并行库实际编写了几段代码,供大家学习参考。

[1] .Net 4.0并行库实用性演练
[2] .Net 4.0并行库实用性演练
[3] .Net 4.0并行库实用性演练

前面说在练习Parallel时,发现另有乾坤,是这样的代码:

view source print?
01 static IEnumerable<Person> testFill()
02 {
03     var list = new List<Person>(9);
04     Enumerable.Range(1, 99999).ToList().ForEach(n =>
05     {
06         var name = "Person" + n % 9;
07         list.Add(new Person { Id = n, Name = name });
08     });
09     Console.WriteLine("Person's count is {0}", list.Count);
10     return list;
11 }
12 static IEnumerable<Person> testFillParallel()
13 {
14     var list = new List<Person>(9);
15     Enumerable.Range(1, 99999).AsParallel().ForAll(n =>
16     {
17         var name = "Person" + n % 9;
18         list.Add(new Person { Id = n, Name = name });
19     });
20     Console.WriteLine("Person's count is {0}", list.Count);
21     return list;
22 }
23 class Person
24 {
25     internal int Id { get; set; }
26     internal string Name { get; set; }
27 }

试验结果如下(单位ms):

次数

1

2

3

4

Fill 方法

37

27

26

26

FillParallel 方法

43

20

19

20

  这个结果有点奇妙的。第一次多线程居然还不如单线程快,和上文例子比较一下,有点明白了。稍微改了下代码,在Add语句前加了个Thread.Sleep(1),并把 List<Person>集合元素减为999,试了一次,结果如下(单位ms):

次数

1

2

3

4

Fill 方法

1012

998

998

999

FillParallel 方法

547

504

504

504

  多个线程协同工作时,分配任务本身有开销,要是分配的开销比任务本身还大,多线程就没有意义了。就比如你交待别人做某件事,要是交待的功夫比自己做还长,还不如自己做。不过从结果也可以看出一个辩证关系,从长远打算,第一次让别人熟悉业务,付出点培训成本,执行完一次后,以后就轻松多了,速度提高了一倍。如果这里Sleep一下,模拟长一点的单次处理过程,一开始多线程的优势就会非常明显。

  FillParallel方法,大家觉得有没有其它问题呢?想必一般人都能看出,这里有最初级的线程安全问题。没看出的应该是刚学.Net各种集合的初学者,线程安全对他们还只是个太虚幻境。不过借助这个Parallel,就可以轻松神游幻境。把FillParallel方法循环一百次执行,会发现返回结果本来应该有999个元素,输出的却显示却结果经常少十几二十个。如果创建List时赋的容量不够,在List扩容时,还可能引发异常。一般是像下图这样(不过一百次都是999也不是不可能,要看你的RP了):

  应提醒一点的是,试验要在Release编译模式下运行,不然看不到线程安全问题,并行执行的效率提升得也很有限。我用的电脑都是双核,不知道在单核电脑的运行情况如何,可能有一定区别。

  接着我改下逻辑,增加了一个是否Person存在重名的判断,变成:

view source print?
01 static IEnumerable<Person> testFillParallel()
02 {
03     var list = new List<Person>(9);
04     Enumerable.Range(1, 999).AsParallel().ForAll(n =>
05     {
06         var name = "Person" + n % 9;
07         if (list.Count(p => p.Name == name) < 1) list.Add(new Person { Id = n, Name = name });
08     });
09     Console.WriteLine("Person's count is {0}", list.Count);
10     return list;
11 }

  RP不管用了,执行几次,必抛异常:System.InvalidOperationException: Collection was modified; enumeration operation may no execute.

  一个线程在枚举集合元素,这时必须保证集合不被其它线程修改,怎么办呢?以前,就知道用锁,现在据说有了线程安全的集合类,在System.Collections.Concurrent命名空间下,有ConcurrentDictionary, ConcurrentQueue, ConcurrentStack,就是没有ConcurrentList。费了半天,才发现与List对应的应该是BlockingCollection。

  把集合定义换成:var list = new BlockingCollection<Person>(9); 只见刷刷刷,哪怕执行几万次都可以一路跑完了。

  不过这样做,还是会发现问题,不知大家看出了吗?

接着上一次说,即使用了新的线程安全的集合BlockingCollection,这段代码还是会有问题。

view source print?
01 static void testFillParallel()
02 {
03     var list = new BlockingCollection<Person>(9999);
04     Enumerable.Range(1, 99999).AsParallel().ForAll(n =>
05     {
06         var name = "Person " + n % 9;
07         if (list.Count(p => p.Name == name) < 1) list.Add(new Person { Id = n, Name = name });
08     });
09     Console.WriteLine("Person's count is {0}", list.Count);
10 }

代码逻辑就是根据序号生成一个名字,并将名字不重复的人加入集合中。显然,最后集合中应该有9个人,应该很简单吧。执行一下,请看结果:

  可见,绝大多数结果都是正确,只有两次执行出现了异常,在正式系统运行中,这可是要命的两条!难道是BlockingCollection的问题吗?细想不太可能,微软怎么也不会漏过这么明显的设计问题,实际上是自己对线程安全认识不准确。集合元素之所以偶尔会多一个,是这样的情况:线程A抢先一步,占住集合判断是否存在这个人名,线程B被BlockingCollection拦在外面;A发现集合中查无此人,正想加一个,然而不知怎么回事,它没马上继续,就好像龟兔赛跑,兔子要到终点了,做了个白日梦,梦一醒自己成了老二。B趁机赶上,正好锁也解了,碰巧它查的人也没有,一鼓作气跑完全程。这时A跑了一大半,岂肯甘心,赖皮着到终点,不管三七二十一,有没有和B刚才加的重复,硬把自己塞了进去。

  这样理解,也容易解释,第一次执行出现异常结果概率很高,因为开始时线程间步调几乎完全一致,刚才故事的前半段最有可能上演。另外条件判断时间越久,异常结果出现概率越小,比如把name = "Person "+ n % 9改成name = "Person "+ n % 50,这时A就是做白日梦,B也全力追赶,无奈前面被落后太多,一千次中只有一两次结果异常。

  其实System.Collections.ConCurrent命名空间底下的类,只对多线程环境下的某次访问保证健壮性,却不能保证多线程下,作为业务对象的业务操作的准确性,实际上也无法做到。然而话说回来,我们使用这些类,是让它们在凶险的多线程战场上,为我们奋勇杀敌,而不是明哲保身的。如果业务出错了,仗都打输了,集合再线程安全亦无济于事。所以,不要让线程安全误导我们,要着眼在业务上,业务安全实现了,自然线程安全自然不在话下。

  现在要解决结果异常,自然又想到了老办法—上锁,先保证万无一失。显然,并行集合也不必上场了,还是用List。为了接近真实场景,将集合最大元素数目提高到999,这样在判断新元素是否重复是要花较多时间,取代Thread.Sleep(1),代码如下:

view source print?
01 static void testFillParallel()
02 {
03     var list = new List<Person>(999);
04     var L = new object();
05     Enumerable.Range(1, 9999).AsParallel().ForAll(n =>
06     {
07         var name = "Person " + n % 999;
08         lock (L)
09         {
10             if (list.Count(p => p.Name == name) < 1) list.Add(new Person { Id = n, Name = name });
11         }
12     });
13     Console.WriteLine("Person's count is {0}", list.Count);
14 }

测试结果如下:

次数

1

2

3

4

Fill 方法

304

292

291

292

FillParallel 方法

340

298

296

297

  可见,虽然运用并行方式,也保证每个迭代有一定的执行时间,虽然加锁可以保证结果正确,但大多数时候,只有一个线程能进行工作,其他线程再多也只能等待,实际上还不如单线程,也失去了并行运算的意义。

  学习.Net4.0 的并行库并非我们的目的,我们目的是解决现实的问题。解决关键,还是在锁上。锁是把双刃剑,我们要让线程占用锁时间尽可能短。在填加集合时,加锁没办法避免,但只是为遍历集合而加锁,好像是种浪费。根据三级锁协议,应该允许多个只读操作同时进行,可是实现IEnumerable的集合,都不允许在遍历访问时修改集合,既然如此,我们自己搞一个集合副本,只作判断用,如果原集合更新,副本也随之更新,不是就能解决同时遍历的问题了吗?

  试验代码:

view source print?
01 static void testFill()
02 {
03     var list = new List<Person>(999);
04     var L = new object();
05     Enumerable.Range(1, 9999).ToList().ForEach(n =>
06     {
07         var name = "Person " + n % 999;
08         if (list.Count(p => p.Name == name) < 1) list.Add(new Person { Id = n, Name = name });
09     });
10     Console.WriteLine("Person's count is {0}", list.Count);
11 }
12 static void testFillParallel()
13 {
14     var list = new List<Person>(999);
15     var L = new object();
16     var resultCopy = new Person[999];
17     bool hasNewMember = false;
18     Enumerable.Range(1, 9999).AsParallel().ForAll(n =>
19     {
20         var name = "Person " + n % 999;
21         if (resultCopy.Count(p => p!=null && p.Name == name) < 1)
22         {
23             lock (L)
24             {
25                 // 如果有新成员,要再判断一遍
26                 if (hasNewMember)
27                 {
28                     if (resultCopy.Count(p => p != null && p.Name == name) > 0) return;
29                     hasNewMember = false;
30                 }
31                 list.Add(new Person { Id = n, Name = name });
32                 list.CopyTo(resultCopy);
33                 hasNewMember = true;
34             }
35         }
36     });
37     Console.WriteLine("Person's count is {0}", list.Count);
38 }

试验结果(单位ms):

次数

1

2

3

4

Fill 方法(和上次一样)

309

291

292

292

FillParallel 方法

210

166

165

166

  看来,自己总算写了第一段能发挥并行运算的代码。

  当然,最后代码还有很多可改进的地方,比如list.CopyTo方法,还有发现有新成员后第二次判断,向让性能与CPU核数正比的目标努力。

  还有,那个if(...){ lock(...){ if(...){的语句,跟单例模式的一种普遍实现很像,单例模式也是多线程环境下一种设计模式,也许其中有些异曲同工之处吧。

.Net 4.0并行库实用性演练[1]相关推荐

  1. Net 4.0并行库实用性演练

    引言 随着CPU多核的普及,编程时充分利用这个特性越显重要.上篇首先用传统的嵌套循环进行数组填充,然后用.NET 4.0中的System.Threading.Tasks提供的Parallel Clas ...

  2. .Net 4.0并行库实用性演练

    前面说在练习Parallel时,发现另有乾坤,是这样的代码: 代码 static IEnumerable<Person> testFill() { var list =new List&l ...

  3. .Net4.0并行库介绍——Cancellation Framework

    在.net 4.0中,引入了一个新的类CancellationToken,这个类基本上集成了我们各种常用的取消方式,在并发任务中非常有用. 同步模式下的取消: 一种比较常见的需要支持取消功能的的是一些 ...

  4. .NET4.0并行计算技术基础(8)

    说明: 要想看懂本系列文章,需要您对.NET多线程开发有基本的了解.我在新书<面向对象的艺术 --.NET Framework 4.0技术剖析与应用>(暂名)中花了近200页的篇幅来介绍. ...

  5. 多线程编程学习笔记——任务并行库(二)

    接上文 多线程编程学习笔记--任务并行库(一) 三.   组合任务 本示例是学习如何设置相互依赖的任务.我们学习如何创建一个任务的子任务,这个子任务必须在父任务执行结束之后,再执行. 1,示例代码如下 ...

  6. 多线程编程学习笔记——任务并行库(三)

    接上文 多线程编程学习笔记--任务并行库(一) 接上文 多线程编程学习笔记--任务并行库(二) 六.   实现取消选项 本示例学习如何实现基于Task的异步操作进行取消流程,以及在任务真正运行前如何知 ...

  7. C#多线程开发-任务并行库

    你好,我是阿辉. 正文共2090字,预计阅读时间:6min. 之前学习了线程池,知道了它有很多好处. 使用线程池可以使我们在减少并行度花销时节省操作系统资源.可认为线程池是一个抽象层,其向程序员隐藏了 ...

  8. 三分钟总览微软任务并行库TPL

    点击上方蓝字进行关注 有小伙伴问我每天忽悠的TPL是什么? ☹️ 这次站位高一点,严肃讲一讲. 引言 俗话说,不想开飞机的程序员不是一名好爸爸:作为微软技术栈的老鸟,一直将代码整洁之道奉为经典, 优秀 ...

  9. .NET异步程序设计之任务并行库

    目录 1.简介 2.Parallel类 2.0 Parallel类简介 2.1 Parallel.For() 2.2 Parallel.ForEach() 2.3 Parallel.Invoke() ...

最新文章

  1. ML:MLOps系列讲解之《CRISP-ML (Q)ML生命周期过程—了解机器学习开发的标准过程模型—业务和数据理解→数据工程(数据准备)→ML模型工程→评估ML模型→模型部署→模型监控和维护》解读
  2. 知识库问答中的关系识别研究回顾
  3. 三星s8和android auto,手机资讯导报:穿上马甲也认得三星GalaxyS8与LGG6再曝光
  4. linux笔记之 raid
  5. Codeforces Round #642 (Div. 3)(AB)
  6. java处理请求的流程_Java Spring mvc请求处理流程详解
  7. C语言最新知识整理(纯干货)
  8. linux搭建nfs
  9. CAMoE——屠榜 video retrieval challenge
  10. python txt转dataframe_Python格式化解析不规则txt文本并转为dataframe
  11. 苹果发文谈iPhone SE的核心竞争力,网友:难道不是便宜吗?
  12. 程序员为什么应该旗帜鲜明地反对“最佳实践”?
  13. 一篇文章告诉你[C++]数组初始化
  14. Ubuntu14.10安装Eclipse
  15. 不是计算机网络教室功能的是,精选:谈计算机网络教室在教学中的应用原稿
  16. No HttpMessageConverter for com.ysd.entity.Users
  17. 百度竞价排名曝光_百度竞价排名和自然排名都是什么?
  18. 傻瓜式学Python3——列表
  19. C语言每日一练——第28天:要求输出国际象棋棋盘
  20. 沟通的艺术02 同理心对话 亲子沟通实例

热门文章

  1. 图论 —— 最短路 —— Dijkstra 算法
  2. 大盗阿福(信息学奥赛一本通-T1301)
  3. 分数线划定(洛谷-P1068)
  4. 信息学奥赛C++语言: 博物馆
  5. 网络基础知识_你家的网络是这么布线的吗?家庭网络布线基础知识普及!
  6. 一起学习C语言:C语言基本语法(三)
  7. 2007年noip普及组初赛试题
  8. arxiv.org经常打不开真是让人头大
  9. pytorch中gather函数的理解
  10. pix2pix损失函数理解(精)