1、Net 4.0并行库实用性演练(前言) 自VS2010发布近半年了,虽然整天想学习新东西,要更新到自己时,发现原来自己基本也很懒,2008还没用上多久呢,无奈被2010 了。用了几天,IDE模样还是和05、08差不多,加了些小特性,以后慢慢体验吧,第一感觉启动速度慢多了。主要还是.Net 4.0的变化,其实也就是修修补补,语言特性几乎没什么新特性,C#多了个Dynamic,十年前VB就支持的晚绑定。只好把注意力放在了 Framework上,新加的并行支持应该是最大的变化吧。 VS2010发布会我也去过的,并行支持是一大卖点。当时记得台上一个MM对一个Linq查询语句加了个AsP
2、arallel(),性能就神奇 地提高了一倍,台下掌声雷动。确实不费吹灰之力提高程序性能,是最能引起大家兴趣的。在看电子期刊时,看到冷冷同学,还有吴秦的文章,给偶这些菜鸟以震撼 的启发,原来偶已经远远落在了在读大学生的后面。 那就开始学吧,就拿Parallel开刀。先抓个垫背的: static void Set(int length) { var array = new int[length, length, length]; for (int i = 0; i < length; i++)
3、 for (int j = 0; j < length; j++) for (int k = 0; k < length; k++) array[i, j, k] = System.Threading.Thread.CurrentThread.ManagedThreadId; } 再请出真神: static void ParallelSet(int length) { var array = ne
4、w int[length, length, length]; Parallel.For(0, length, i => { for (int j = 0; j < length; j++) for (int k = 0; k < length; k++) array[i, j, k] = System.Threading.Thread.CurrentThread.ManagedThreadId;
5、 }); } PK: CodeTimer.Time("Single thread", 100, () => Set(100)); CodeTimer.Time("Multiple thread", 100, () => ParallelSet(100)); 结果,1136ms:729ms,果然不错。不过MSDN的例子说不定是被和谐过的,所以偶总会变变试验过程。果然发现另有乾坤。 .Net 4.0并行库实用性演练 前面说在练习Parallel时,发现另有乾坤,是这样的代码: 代码 static I
6、Enumerable
7、 return list;
}
static IEnumerable
8、rson's count is {0}", list.Count); return list; } class Person { internal int Id { get; set; } internal string Name { get; set; } } 试验结果如下(单位ms): 次数 1 2 3 4 Fill 方法 37 27 26 26 FillParallel 方法 43 20 19 20 这个结果有点奇妙的。第一次多线程居然还不如单线程快,和上文例子比较一下,有点
9、明白了。稍微改了下代码,在Add语句前加了个Thread.Sleep(1),并把 List
10、培训成本,执行完一次后,以后就轻松多了,速度提 高了一倍。如果这里Sleep一下,模拟长一点的单次处理过程,一开始多线程的优势就会非常明显。 FillParallel方法,大家 觉得有没有其它问题呢?想必一般人都能看出,这里有最初级的线程安全问题。没看出的应该是刚学.Net各种集合的初学者,线程安全对他们还只是个太虚幻 境。不过借助这个Parallel,就可以轻松神游幻境。把FillParallel方法循环一百次执行,会发现返回结果本来应该有999个元素,输出的 却显示却结果经常少十几二十个。如果创建List时赋的容量不够,在List扩容时,还可能引发异常。一般是像下图这样(不过一百次都
11、是999也不是不可 能,要看你的RP了):
应提醒一点的是,试验要在Release编译模式下运行,不然看不到线程安全问题,并行执行的效率提升得也很有限。我用的电脑都是双核,不知道在单核电脑的运行情况如何,可能有一定区别。
接着我改下逻辑,增加了一个是否Person存在重名的判断,变成:
代码
static IEnumerable
12、> { var name = "Person" + n % 9; if (list.Count(p => p.Name == name) < 1) list.Add(new Person { Id = n, Name = name }); }); Console.WriteLine("Person's count is {0}", list.Count); return list; } RP不管用了,执行几次,必抛异常:System.InvalidOperationException: Collection w
13、as modified; enumeration operation may no execute. 一个线程在枚举集合元素,这时必须保证集合不被其它线程修改,怎么办呢?以前,就知道用锁,现在据说有了线程安全的集合类,在 System.Collections.Concurrent命名空间下,有ConcurrentDictionary, ConcurrentQueue, ConcurrentStack,就是没有ConcurrentList。费了半天,才发现与List对应的应该是 BlockingCollection。 把集合定义换成: var list = new BlockingC
14、ollection
15、对比发现其中差异。主要内容如下: · 通常的数组填充 · 并行的组数填充 · 性能比较 · System.Threading.Tasks分析,这个将在续篇.NET(C#) Internals: 以一个数组填充的例子初步了解.NET 4.0中的并行(二)中介绍 1、通常的数组填充 首先看如下代码: 通常的数组填充 using System; namespace ParallelForSample { public class SingleCore { public static void Calculate(int calcVal)
16、{ Utility util = new Utility(); util.Start(); int[,] G = new int[calcVal, calcVal]; for (int k = 0; k < calcVal; k++) for (int i = 0; i < calcVal; i++) for (int j = 0; j < calcVal; j++) G[i, j] = Math.Min(G[i, j], G[i, k] + G[k, j]);
17、 util.Stop(); } } } 上面的粗体红色显示的几行代码就是实现数组填充,这个很好理解不用多费口舌。补充说明的是:上面的Utility是为了统计性能而编写的一个类,它主要就是用到了Stopwatch对象——它提供一组方法和属性,可用于准确地测量运行时间。Utility的代码如下: Utility类 public class Utility { private Stopwatch _stopwatch; public void Start() { _
18、stopwatch = new Stopwatch(); _stopwatch.Start(); } public void Stop() { _stopwatch.Stop(); TimeSpan ts = _stopwatch.Elapsed; string elapsedTime = String.Format("{0:00}:{1:00}:{2:00}.{3:00}", ts.Hours, t
19、s.Minutes, ts.Seconds, ts.Milliseconds / 10); Console.WriteLine("Time taken : {0}", elapsedTime); } } 利用它我们就可以对数组填充所耗费的时间进行计算了。 2、并行的组数填充 为了充分利用CPU的多核,我们编写如下代码: 并行的数组填充 using System; using System.Threading.Tasks; namespace ParallelForSample {
20、 public class MultiCore { public static void Calculate(int calcVal) { Utility util = new Utility(); util.Start(); int[,] G = new int[calcVal, calcVal]; Parallel.For(0, calcVal, delegate(int k) { Parallel.For(0, calcVal, delega
21、te(int i)
{
for (int j = 0; j < calcVal; j++)
G[i, j] = Math.Min(G[i, j], G[i, k] + G[k, j]);
});
}
);
util.Stop();
}
}
}
留意上面的红色粗体显示的几行代码,它利用了Parallel.For Method (Int32, Int32, Action
22、tem.Threading.Tasks中,它支持并行循环。此Parallel.For方法使得它里面的迭代可能并行地运行,注意到上述代码中它的第三个参数是一个委托。在(0,calcVal)之间,这个委托将被调用。 3、性能比较 现在我们来测试一下,上面两种方法的执行性能差异如何,下载源码。其实,核心代码已经在上面贴出来了,现在注意是编写实例来测试,代码主要如下: 性能比较测试 using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Para
23、llelForSample { class Program { static void Main(string[] args) { Console.WriteLine("Single core"); SingleCore.Calculate(1000); Console.WriteLine("Multi core"); MultiCore.Calculate(1000); Console.WriteLine(
24、"Finished"); Console.ReadKey(); } } } 运行之后得到如下结果:(不同电脑配置不同,得出结果不同) 图1、性能比较 从结果可以看出,并行的数组填充比通常的数组填充性能更高。 · System.Threading.Tasks分析,这个将在续篇.NET(C#) Internals: 以一个数组填充的例子初步了解.NET 4.0中的并行(二)中介绍…… 作者:吴秦 出处: 本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名吴秦(包含
25、链接). .NET(C#) Internals: 以一个数组填充的例子初步了解.NET 4.0中的并行(二) 2010-05-12 21:54 by 吴秦, 1190 visits, 网摘, 收藏, 编辑 引言 随着CPU多核的普及,编程时充分利用这个特性越显重要。上篇首先用传统的嵌套循环进行数组填充,然后用.NET 4.0中的System.Threading.Tasks提供的Parallel Class来并行地进行填充,最后对比他们的性能。本文将深入分析Parallel Class并借机回答上篇9楼提出的问题,而System.Threading.Tasks分析,这个将推迟到.NET
26、C#) Internals: 以一个数组填充的例子初步了解.NET 4.0中的并行(三)中介绍。内容如下: · 1、Parallel Class o 1.1、For方法 o 1.2、ForEach方法 o 1.3、Invoke方法 · 2、并发控制疑问? o 2.1、使用Lock锁 o 2.2、使用PLINQ——用AsParallel o 2.3、使用PLINQ——用ParallelEnumerable o 2.4、使用Interlocked操作 o 2.5、使用Parallel.For的有Thread-Local变量重载函数 · 性能比较 1、Para
27、llel Class Parallel——这个类提供对通常操作(诸如for、foreach、执行语句块)基于库的数据并行替换。它只是System.Threading.Tasks命名空间的一个类,该命名空间中还包括很多其他的类。下面举个例子来说明如何使用Parallel.For(来自MSDN): view source print? 01 using System.Threading.Tasks; 02 class Test 03 { 04 static int N = 1000; 05 06 static void TestMe
28、thod() 07 { 08 // Using a named method. 09 Parallel.For(0, N, Method2); 10 11 // Using an anonymous method. 12 Parallel.For(0, N, delegate(int i) 13 { 14 // Do Work. 15 }); 16 17 // Using a
29、lambda expression. 18 Parallel.For(0, N, i => 19 { 20 // Do Work. 21 }); 22 } 23 24 static void Method2(int i) 25 { 26 // Do work. 27 } 28 } 上面这个例子简单易懂,上篇我们就是用的Parallel.For,这里就不解释了。其实Parallel类的方法主要分为下面三类:
30、
· For方法
· ForEach方法
· Invoke方法
1.1、For方法
在里面执行的for循环可能并行地运行,它有12个重载。这12个重载中Int32参数和Int64参数的方法各为6个,下面以Int32为例列出:
· For(Int32 fromInclusive, Int32 toExclusive, Action
31、
· For(Int32 fromInclusive, Int32 toExclusive, Action
32、调用break跳出传统的for循环,不是break的原因是它不保证当前迭代之后的迭代绝对不会执行。
如果在当前迭代之前的迭代不必要执行,应该调用Stop而不是Break。调用Stop通知For循环放弃剩下的迭代,不管它是否在当前迭代之前或之后,因为所以要求的工作已经完成。然而,Break并不能保证这个。
· For(Int32 fromInclusive, Int32 toExclusive, ParallelOptions parallelOptions, Action
33、
· For(Int32 fromInclusive, Int32 toExclusive, ParallelOptions parallelOptions, Action
34、tion
35、将被调用。这个委托可能在多个线程上并发执行,因此,你必须同步访问任何共享变量。
· For
36、urceview source print? 01 using System; 02 using System.Collections.Generic; 03 using System.Linq; 04 using System.Text; 05 using System.Threading; 06 using System.Threading.Tasks; 07 08 namespace ConsoleApplication2 09 { 10 class Program 11 { 12
37、// Demonstrated features: 13 // CancellationTokenSource 14 // Parallel.For() 15 // ParallelOptions 16 // ParallelLoopResult 17 // Expected results: 18 // An iteration for each argument value (0, 1,
38、 2, 3, 4, 5, 6, 7, 8, 9) is executed. 19 // The order of execution of the iterations is undefined. 20 // The iteration when i=2 cancels the loop. 21 // Some iterations may bail out or not start at all; because they are temporally executed in unp
39、redictable order, 22 // it is impossible to say which will start/complete and which won't. 23 // At the end, an OperationCancelledException is surfaced. 24 // Documentation: 25 // 26 27 static void Main(string[] arg
40、s) 28 { 29 CancellationTokenSource cancellationSource = new CancellationTokenSource(); 30 ParallelOptions options = new ParallelOptions(); 31 options.CancellationToken = cancellationSource.Token; 32 try 33 { 34
41、 ParallelLoopResult loopResult = Parallel.For( 35 0, 36 10, 37 options, 38 (i, loopState) => 39 { 40 Console.WriteLine("Start Thread={0}, i={1}"
42、 Thread.CurrentThread.ManagedThreadId, i); 41 42 // Simulate a cancellation of the loop when i=2 43 if (i == 2) 44 { 45 cancellationSource.Cancel(); 46 }
43、 47 48 // Simulates a long execution 49 for (int j = 0; j < 10; j++) 50 { 51 Thread.Sleep(1 * 200); 52 53 // check to see whether or not to continue
44、 54 if (loopState.ShouldExitCurrentIteration) return; 55 } 56 57 Console.WriteLine("Finish Thread={0}, i={1}", Thread.CurrentThread.ManagedThreadId, i); 58 } 59 ); 60
45、 if (loopResult.IsCompleted) 61 { 62 Console.WriteLine("All iterations completed successfully. THIS WAS NOT EXPECTED."); 63 } 64 } 65 // No exception is expected in this example, but if on
46、e is still thrown from a task, 66 // it will be wrapped in AggregateException and propagated to the main thread. 67 catch (AggregateException e) 68 { 69 Console.WriteLine("Parallel.For has thrown an AggregateException. THIS WAS NOT
47、EXPECTED.\n{0}", e); 70 } 71 // Catching the cancellation exception 72 catch (OperationCanceledException e) 73 { 74 Console.WriteLine("An iteration has triggered a cancellation. THIS WAS EXPECTED.\n{0}", e.ToString());
48、 75 } 76 } 77 } 78 } 1.2、ForEach方法 在迭代中执行的foreach操作可能并行地执行,它有20个重载。这个方法太多,但用法大概跟For方法差不多,请自行参考MSDN。 1.3、Invoke方法 提供的每个动作可能并行地执行,它有2个重载。 · Invoke(params Action[] actions):actions是一个要执行的动作数组,这些动作可能并行地执行,但并不保证执行的顺序及一定并行执行。这个方法直到提供的所有操作完成时才返回,不管是否正常地完成或异常终止。 ·
49、Invoke(ParallelOptions parallelOptions, params Action[] actions):跟上面的方法类似,只是增加了一个parallelOptions参数,可以用户调用者取消整个操作。 例如下面代码执行了三个操作(来自MSDN): view source print? 01 using System; 02 using System.Collections.Generic; 03 using System.Linq; 04 using System.Text; 05 using System.Threading; 06 using System.Threading.Tasks; 07 08 namespace ConsoleApplication2 09 { 10 class Program 11 { 12 static void Main() 13 { 14






