随着多核时代的到来,并行开发越来越展示出它的强大威力!使用并行程序,充分的利用系统资源,提高程序的性能。在.net 4.0中,微软给我们提供了一个新的命名空间:System.Threading.Tasks。这里面有很多关于并行开发的东西,今天第一篇就介绍下最基础,最简单的——认识和使用Parallel。


一、 Parallel的使用

在Parallel下面有三个常用的方法invoke,For和ForEach。


1、Parallel.Invoke

这是最简单,最简洁的将串行的代码并行化。

在这里先讲一个知识点,就是StopWatch的使用,最近有一些人说找不到StopWatch,StopWatch到底是什么东西,今天就来说明一下。

StopWatch在System.Diagnostics命名控件,要使用它就要先引用这个命名空间。

其使用方法如下:


1

2

3

4

5

6

7

8

9

10

11



​var stopWatch = ​​​​new​​​ ​​StopWatch();   ​​​​//创建一个Stopwatch实例​


​stopWatch.Start();   ​​​​//开始计时​


​stopWatch.Stop();   ​​​​//停止计时​


​stopWatch.Reset();  ​​​​//重置StopWatch​


​stopWatch.Restart(); ​​​​//重新启动被停止的StopWatch​


​stopWatch.ElapsedMilliseconds ​​​​//获取stopWatch从开始到现在的时间差,单位是毫秒​


本次用到的就这么多知识点,想了解更多关于StopWatch的,去百度一下吧,网上有很多资料。

下面进入整体,开始介绍Parallel.Invoke方法,废话不多说了,首先新建一个控制台程序,添加一个类,代码如下:


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29



​public​​​ ​​class​​​ ​​ParallelDemo​

​{​

​private​​​ ​​Stopwatch stopWatch = ​​​​new​​​ ​​Stopwatch();​


​public​​​ ​​void​​​ ​​Run1()​

​{​

​Thread.Sleep(2000);​

​Console.WriteLine(​​​​"Task 1 is cost 2 sec"​​​​);​

​}​

​public​​​ ​​void​​​ ​​Run2()​

​{​

​Thread.Sleep(3000);​

​Console.WriteLine(​​​​"Task 2 is cost 3 sec"​​​​);​

​}​


​public​​​ ​​void​​​ ​​ParallelInvokeMethod()​

​{​

​stopWatch.Start();​

​Parallel.Invoke(Run1, Run2);​

​stopWatch.Stop();​

​Console.WriteLine(​​​​"Parallel run "​​​ ​​+ stopWatch.ElapsedMilliseconds + ​​​​" ms."​​​​);​


​stopWatch.Restart();​

​Run1();​

​Run2();​

​stopWatch.Stop();​

​Console.WriteLine(​​​​"Normal run "​​​ ​​+ stopWatch.ElapsedMilliseconds + ​​​​" ms."​​​​);​

​}​

​}​


代码很简单,首先新加一个类,在类中写了两个方法,Run1和Run2,分别等待一定时间,输出一条信息,然后写了一个测试方法ParallelInvokeMethod,分别用两种方法调用Run1和Run2,然后在main方法中调用,下面来看一下运行时间如何:

c# 并行和多线程编程——认识Parallel_异常信息

  大家应该能够猜到,正常调用的话应该是5秒多,而Parallel.Invoke方法调用用了只有3秒,也就是耗时最长的那个方法,可以看出方法是并行执行的,执行效率提高了很多。


2、Parallel.For

这个方法和For循环的功能相似,下面就在类中添加一个方法来测试一下吧。代码如下:


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28



​public​​​ ​​void​​​ ​​ParallelForMethod()​

​{​

​stopWatch.Start();​

​for​​​ ​​(​​​​int​​​ ​​i = 0; i < 10000; i++)​

​{​

​for​​​ ​​(​​​​int​​​ ​​j = 0; j < 60000; j++)​

​{​

​int​​​ ​​sum = 0;​

​sum += i;​

​}​

​}​

​stopWatch.Stop();​

​Console.WriteLine(​​​​"NormalFor run "​​​ ​​+ stopWatch.ElapsedMilliseconds + ​​​​" ms."​​​​);​


​stopWatch.Reset();​

​stopWatch.Start();​

​Parallel.For(0, 10000, item =>​

​{​

​for​​​ ​​(​​​​int​​​ ​​j = 0; j < 60000; j++)​

​{​

​int​​​ ​​sum = 0;​

​sum += item;​

​}​

​});​

​stopWatch.Stop();​

​Console.WriteLine(​​​​"ParallelFor run "​​​ ​​+ stopWatch.ElapsedMilliseconds + ​​​​" ms."​​​​);​


​}​


写了两个循环,做了一些没有意义的事情,目的主要是为了消耗CPU时间,同理在main方法中调用,运行结果如下图:

c# 并行和多线程编程——认识Parallel_sed_02

可以看到,Parallel.For所用的时间比单纯的for快了1秒多,可见提升的性能是非常可观的。那么,是不是Parallel.For在任何时候都比for要快呢?答案当然是“不是”,要不然微软还留着for干嘛?

下面修改一下代码,添加一个全局变量num,代码如下:


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37



​public​​​ ​​void​​​ ​​ParallelForMethod()​

​{​

​var obj = ​​​​new​​​ ​​Object();​

​long​​​ ​​num = 0;​

​ConcurrentBag<​​​​long​​​​> bag = ​​​​new​​​ ​​ConcurrentBag<​​​​long​​​​>();​


​stopWatch.Start();​

​for​​​ ​​(​​​​int​​​ ​​i = 0; i < 10000; i++)​

​{​

​for​​​ ​​(​​​​int​​​ ​​j = 0; j < 60000; j++)​

​{​

​//int sum = 0;​

​//sum += item;​

​num++;​

​}​

​}​

​stopWatch.Stop();​

​Console.WriteLine(​​​​"NormalFor run "​​​ ​​+ stopWatch.ElapsedMilliseconds + ​​​​" ms."​​​​);​


​stopWatch.Reset();​

​stopWatch.Start();​

​Parallel.For(0, 10000, item =>​

​{​

​for​​​ ​​(​​​​int​​​ ​​j = 0; j < 60000; j++)​

​{​

​//int sum = 0;​

​//sum += item;​

​lock​​​ ​​(obj)​

​{​

​num++;​

​}​

​}​

​});​

​stopWatch.Stop();​

​Console.WriteLine(​​​​"ParallelFor run "​​​ ​​+ stopWatch.ElapsedMilliseconds + ​​​​" ms."​​​​);​


​}​


Parallel.For由于是并行运行的,所以会同时访问全局变量num,为了得到正确的结果,要使用lock,此时来看看运行结果:

c# 并行和多线程编程——认识Parallel_异常处理_03

  是不是大吃一惊啊?Parallel.For竟然用了15秒多,而for跟之前的差不多。这主要是由于并行同时访问全局变量,会出现资源争夺,大多数时间消耗在了资源等待上面。

一直说并行,那么从哪里可以看出来Parallel.For是并行执行的呢?下面来写个测试代码:


1

2

3

4



​Parallel.For(0, 100, i =>​

​{​

​Console.Write(i + ​​​​"\t"​​​​);​

​});​


从0输出到99,运行后会发现输出的顺序不对,用for顺序肯定是对的,并行同时执行,所以会出现输出顺序不同的情况。


3、Parallel.Foreach

这个方法跟Foreach方法很相似,想具体了解的,可以百度些资料看看,这里就不多说了,下面给出其使用方法:


1

2

3

4

5

6



​List<​​​​int​​​​> list = ​​​​new​​​ ​​List<​​​​int​​​​>();​

​list.Add(0);​

​Parallel.ForEach(list, item =>​

​{​

​DoWork(item);​

​});​



二、 Parallel中途退出循环和异常处理


1、当我们使用到Parallel,必然是处理一些比较耗时的操作,当然也很耗CPU和内存,如果我们中途向停止,怎么办呢?

在串行代码中我们break一下就搞定了,但是并行就不是这么简单了,不过没关系,在并行循环的委托参数中提供了一个ParallelLoopState,

该实例提供了Break和Stop方法来帮我们实现。

Break: 当然这个是通知并行计算尽快的退出循环,比如并行计算正在迭代100,那么break后程序还会迭代所有小于100的。

Stop:这个就不一样了,比如正在迭代100突然遇到stop,那它啥也不管了,直接退出。

下面来写一段代码测试一下:


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16



​public​​​ ​​void​​​ ​​ParallelBreak()​

​{​

​ConcurrentBag<​​​​int​​​​> bag = ​​​​new​​​ ​​ConcurrentBag<​​​​int​​​​>();​

​stopWatch.Start();​

​Parallel.For(0, 1000, (i, state) =>​

​{​

​if​​​ ​​(bag.Count == 300)​

​{​

​state.Stop();​

​return​​​​;​

​}​

​bag.Add(i);​

​});​

​stopWatch.Stop();​

​Console.WriteLine(​​​​"Bag count is "​​​ ​​+ bag.Count + ​​​​", "​​​ ​​+ stopWatch.ElapsedMilliseconds);​

​}​


这里使用的是Stop,当数量达到300个时,会立刻停止;可以看到结果"Bag count is 300",如果用break,可能结果是300多个或者300个,大家可以测试一下。


2、异常处理

  首先任务是并行计算的,处理过程中可能会产生n多的异常,那么如何来获取到这些异常呢?普通的Exception并不能获取到异常,然而为并行诞生的AggregateExcepation就可以获取到一组异常。

这里我们修改Parallel.Invoke的代码,修改后代码如下:


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49



​public​​​ ​​class​​​ ​​ParallelDemo​

​{​

​private​​​ ​​Stopwatch stopWatch = ​​​​new​​​ ​​Stopwatch();​


​public​​​ ​​void​​​ ​​Run1()​

​{​

​Thread.Sleep(2000);​

​Console.WriteLine(​​​​"Task 1 is cost 2 sec"​​​​);​

​throw​​​ ​​new​​​ ​​Exception(​​​​"Exception in task 1"​​​​);​

​}​

​public​​​ ​​void​​​ ​​Run2()​

​{​

​Thread.Sleep(3000);​

​Console.WriteLine(​​​​"Task 2 is cost 3 sec"​​​​);​

​throw​​​ ​​new​​​ ​​Exception(​​​​"Exception in task 2"​​​​);​

​}​


​public​​​ ​​void​​​ ​​ParallelInvokeMethod()​

​{​

​stopWatch.Start();​

​try​

​{​

​Parallel.Invoke(Run1, Run2);​

​}​

​catch​​​ ​​(AggregateException aex)​

​{​

​foreach​​​ ​​(var ex ​​​​in​​​ ​​aex.InnerExceptions)​

​{​

​Console.WriteLine(ex.Message);​

​}​

​}​

​stopWatch.Stop();​

​Console.WriteLine(​​​​"Parallel run "​​​ ​​+ stopWatch.ElapsedMilliseconds + ​​​​" ms."​​​​);​


​stopWatch.Reset();​

​stopWatch.Start();​

​try​

​{​

​Run1();​

​Run2();​

​}​

​catch​​​​(Exception ex)​

​{​

​Console.WriteLine(ex.Message);​

​}​

​stopWatch.Stop();​

​Console.WriteLine(​​​​"Normal run "​​​ ​​+ stopWatch.ElapsedMilliseconds + ​​​​" ms."​​​​);​

​}​

​}​


顺序调用方法我把异常处理写一起了,这样只能捕获Run1的异常信息,大家可以分开写。捕获AggregateException 异常后,用foreach循环遍历输出异常信息,可以看到两个异常信息都显示了。

​点击这里,下载源码​

以上就是c# 并行和多线程编程——认识Parallel的详细内容,更多关于c# 并行和多线程编程的资料请关注脚本之家其它相关文章!