在上篇文章中我们看过了多线程中的数据共享问题,以及通过分离执行来解决的办法。本篇文章就数据共享的同步处理中的一些常见的方法进行一些介绍。
数据同步时的步骤
我之前的文章我们对数据共享问题处理的方式是“分离执行”,我们通过把每个Task执行完成后的各自计算的值进行最后的汇总,也就是说多个Task之间不存在数据共享了,各自做各自的事,完全分离开来。可是这毕竟不是我们常规的处理办法,这样我们会花更多的经历在分离上。其实我们主要的数据共享在Sum上:
for (int j = 0; j < 1000; j++) { Sum++; }
那我们可以通过一种有序的机制来访问Sum,即当一个Task在进行Sum访问时,其它的Task做等待,从达成对Sum的有序访问,避免对Sum的争用。准备做同步处理时,我们一般要做两个步骤:
1)确定数据共享的临界区域
2)确定使用的同步的类型
比如在之前的例子中,For语句中的Sum++就是临界区域,在这个地方可能会出现对Sum的争用。所以我们可以确定For语句中的区域为一个数据共享的临界区域。确定完成临界区域之后,我们需要决定使用那种同步的类型。这里首先解释下什么是同步类型,同步类型是一种用来调度Task访问临界区域的一种特殊类型。在.Net 4.0中提供了多种同步类型给我们使用,主要分为:轻量级的、重量级的和等待处理型的,在下面我们会介绍常用的同步处理类型。
常用的同步类型
首先来看看.Net 4.0中常见的几种同步类型以及处理的相关问题:
同步类型 | 解决问题 |
lock关键字、Montor类、SpinLock类 | 有序访问临界区域 |
Interlocked类 | 数值类型的增加或则减少 |
Mutex类 | 交叉同步 |
WaitAll方法 | 同步多个锁定(主要是Task之间的调度) |
申明性的同步(如Synchronization) | 使类中的所有的方法同步 |
下面我们就详细常见的同步类型的使用:
Lock
其实最简单同步类型的使用办法就是使用Lock关键字。在使用lock关键字时,首先我们需要创建一个锁定的object,而且这个object需要所有的task都能访问,其次能我们需要将我们的临界区域包含在lock块中。我们之前例子中代码可以这样加上lock:
int Sum = 0; Task[] tasks = new Task[10]; var obj = new Object(); for (int i = 0; i < 10; i++) { tasks[i] = new Task(() => { for (int j = 0; j < 1000; j++) { lock (obj) { Sum = Sum + 1; } } }); tasks[i].Start(); } Task.WaitAll(tasks); Console.WriteLine("Expected value {0}, Parallel value: {1}", 10000, Sum);
其实lock关键字是使用Monitor的一种简短的方式,其实上面的Lock部分可以理解成:
try { Monitor.Enter(obj,ref lockAcquired); Sum = Sum + 1; } finally { if (lockAcquired) Monitor.Exit(obj); }
lock关键字自动通过调用Monitor.Enter,Monitor.Exit方法来处理获得锁以及释放锁,在Enter方法中传递了bool值类型的参数,这个参数会在获得锁之后设置成True,我们在调用Exit是需要确认这个参数的值的。
Interlocked
Interlocked其实在之前的文章的我们已经使用过,Interlocked通过使用操作系统或则硬件的一些特性提供了一些列高效的静态的同步方法。其中主要提供了这些方法:Exchange、Add、Increment、CompareExchange四种类型的多个方法的重载。我们将上面的例子中使用Interlocked:
int Sum = 0; Task[] tasks = new Task[10]; var obj = new Object(); for (int i = 0; i < 10; i++) { tasks[i] = new Task(() => { for (int j = 0; j < 1000; j++) { Interlocked.Increment(ref Sum); } }); tasks[i].Start(); } Task.WaitAll(tasks); Console.WriteLine("Expected value {0}, Parallel value: {1}", 10000, Sum);
Mutex
Mutex也是一个同步类型,在多个线程进行访问的时候,它只向一个线程授权共享数据的独立访问。我们可以通过Mutex中的WaitOne方法来获取Mutex的所有权,但是同时我们要注意的是,我们在一个线程中多少次调用过WaitOne方法,就需要调用多少次ReleaseMutex方法来释放Mutex的占有。上面的例子我们通过Mutex这样实现:
int Sum = 0; Task[] tasks = new Task[10]; var mutex = new Mutex(); for (int i = 0; i < 10; i++) { tasks[i] = new Task(() => { for (int j = 0; j < 1000; j++) { bool lockAcquired = mutex.WaitOne(); try { Sum++; } finally { if (lockAcquired) mutex.ReleaseMutex(); } } }); tasks[i].Start(); } Task.WaitAll(tasks); Console.WriteLine("Expected value {0}, Parallel value: {1}", 10000, Sum);
同步类型使用建议
在上面的我们已经介绍过了三种同步类型的使用,其中想Lock这样的关键字是轻量级的,而Monitor则是重量级的,需要我们自己进行手动的获取锁释放锁,在使用同步类型的时候建议大家选择轻量级的类型。下面有几点建议让大家更加高效地同步:
1.更多的使用轻量级的同步类型
2.尽量不要使用自定义同步类型
3.不要同步得太多也不要同步得太少
总结
在本篇文章我们看过如何进行Task中的一些同步处理,同时看过常用的三种同步类型,希望对大家有帮助。在下篇文章中会介绍下其他的一些同步处理,以及一些常见的问题的处理。