.Net线程同步的n个方法(一)
本文主要描述在C#中线程同步的方法。线程的基本概念在上一章中已经介绍过了,网上资料也很多就不再赘述了。直接接入主题,在多线程开发的应用中,线程同步是不可避免的。在.Net框架中,实现线程同步主要通过以下的几种方式来实现,在MSDN的线程指南中已经讲了几种,本文结合作者实际中用到的方式一起说明一下。
1. 维护自由锁(InterLocked)实现同步
2. 监视器(Monitor)和互斥锁(lock)
3. 读写锁(ReadWriteLock)
4. 系统内核对象
1) 互斥(Mutex), 信号量(Semaphore), 事件(AutoResetEvent/ManualResetEvent)
2) 线程池
除了以上的这些对象之外实现线程同步的还可以使用Thread.Join方法。这种方法比较简单,当你在第一个线程运行时想等待第二个线程执行结果,那么你可以让第二个线程Join进来就可以了。
@_@自由锁(InterLocked)
对一个32位的整型数进行递增和递减操作来实现锁,有人会问为什么不用++或--来操作。因为在多线程中对锁进行操作必须是原子的,而++和--不具备这个能力。因此Increment(ref counter)这样的操作是线程安全的。InterLocked类还提供了两个另外的函数Exchange, CompareExchange用于实现交换和比较交换。Exchange操作会将新值设置到变量中并返回变量的原来值:int oVal = InterLocked.Exchange(ref val, 1)。
@_@监视器(Monitor)
在MSDN中对Monitor的描述是: Monitor 类通过向单个线程授予对象锁来控制对对象的访问。
Monitor类是一个静态类因此你不能通过实例化来得到类的对象。Monitor的成员可以查看MSDN,基本上Monitor的效果和lock是一样的,通过加锁操作Enter设置临界区,完成操作后使用Exit操作来释放对象锁。不过相对的来说Monitor的功能更强,Moniter可以进行测试锁的状态,因此你可以控制对临界区的访问选择,等待or离开, 而且Monitor还可以在释放锁之前通知指定的对象,更重要的是使用Monitor可以跨越方法来操作。Monitor提供的方法很少就只有获取锁的方法Enter, TryEnter;释放锁的方法Wait, Exit;还有消息通知方法Pulse, PulseAll。经典的Monitor操作是这样的
2static public void DelUser(string name)
3{
4 try
5 {
6 // 等待线程进入
7 Monitor.Enter(Names);
8 Names.Remove(name);
9 Console.WriteLine("Del: {0}", Names.Count);
10 Monitor.Pulse(Names);
11 }
12 finally
13 {
14 // 释放对象锁
15 Monitor.Exit(Names);
16 }
17}
其中Names是一个List<string>, 这里有一个小技巧,如果你想声明整个方法为线程同步可以使用方法属性
2[MethodImpl(MethodImplOptions.Synchronized)]
3static public void AddUser(string name)
4{
5 Names.Add(name);
6 Console.WriteLine("Add: {0}",Names.Count);
7}
对于Monitor的使用有一个方法是比较诡异的,那就是Wait方法。在MSDN中对Wait的描述是:
释放对象上的锁以便允许其他线程锁定和访问该对象。
这里提到的是先释放锁,那么显然我们需要先得到锁,否则调用Wait会出现异常,所以我们必须在Wait前面调用Enter方法或其他获取锁的方法如lock,这点很重要。对应Enter方法Monitor给出来另一种实现TryEnter。这两种方法的主要区别在与是否阻塞当前线程,Enter方法在当获取不到锁时,会阻塞当前线程直到得到锁。不过缺点是如果永远得不到锁那么程序就会进入死锁状态,我们可以采用Wait来解决,在调用Wait时加入超时时限就可以。
2{
3 Monitor.Wait(Names, 1000); // !!
4 Names.Remove(name);
5 Console.WriteLine("Del: {0}", Names.Count);
6 Monitor.Pulse(Names);
7}
@_@ 互斥锁(lock)
lock关键字是实现线程同步的比较简单的方式,其实就是设置一个临界区。在lock之后的{...}区块为一个临界区,在进入临界区是加互斥锁离开临界区时释放互斥锁。MSDN对lock关键字的描述是:
lock 关键字可将语句块标记为临界区,方法是获取给定对象的互斥锁,执行语句,然后释放该锁。
具体例子如下:
2{
3 string str = name as string;
4 Random rand = new Random();
5 int count = rand.Next(100, 200);
6
7 for (int i = 0; i < count; i++)
8 {
9 lock (NumList)
10 {
11 NumList.Add(i);
12 Console.WriteLine("{0} {1}", str, i);
13 }
14 }
15}
对lock的使用有几点建议:对实例锁定lock(this),对静态变量锁定lock(typeof(val))。lock的对象访问权限最好是private,否则会出现失去访问控制现象。
@_@读写锁(ReadWriteLock)
读写锁的出现主要是在很多情况下,我们读资源的操作要多于写资源的操作。但是如果每次只对资源赋予一个线程的访问权限显然是低效的,读写锁的优势是同时可以有多个线程对同一资源进行读操作。因此在读操作比写操作多很多,并且写操作的时间很短的情况下使用读写锁是比较有效率的。读写锁是一个非静态类所以你在使用前需要先声明一个读写锁对象:
static private ReaderWriterLock _rwlock = new ReaderWriterLock();
读写锁是通过调用AcquireReaderLock,ReleaseReaderLock,AcquireWriterLock,ReleaseWriterLock来完成读锁和写锁控制的。
2{
3 try
4 {
5 // 请求读锁,如果100ms超时退出
6 _rwlock.AcquireReaderLock(10);
7 try
8 {
9 int inx = _rand.Next(_list.Count);
10 if (inx < _list.Count)
11 Console.WriteLine("{0}thread {1}", thrdId, _list[inx]);
12 }
13 finally
14 {
15 _rwlock.ReleaseReaderLock();
16 }
17 }
18 catch (ApplicationException) // 如果请求读锁失败
19 {
20 Console.WriteLine("{0}thread get reader lock out time!", thrdId);
21 }
22}
23
24static public void WriterThread()
25{
26 try
27 {
28 // 请求写锁
29 _rwlock.AcquireWriterLock(100);
30 try
31 {
32 string val = _rand.Next(200).ToString();
33 _list.Add(val); // 写入资源
34 Console.WriteLine("writer thread has written {0}", val);
35 }
36 finally
37 {
38 // 释放写锁
39 _rwlock.ReleaseWriterLock();
40 }
41 }
42 catch (ApplicationException)
43 {
44 Console.WriteLine("Get writer thread lock out time!");
45 }
46}
如果你想在读的时候插入写操作请使用UpgradeToWriterLock和DowngradeFromWriterLock来进行操作,而不是释放读锁。
2{
3 try
4 {
5 _rwlock.AcquireReaderLock(10);
6 try
7 {
8 try
9 {
10 // 提升读锁到写锁
11 LockCookie lc = _rwlock.UpgradeToWriterLock(100);
12 try
13 {
14 string val = _rand.Next(500).ToString();
15 _list.Add(val);
16 Console.WriteLine("Upgrade Thread{0} add {1}", thrdId, val);
17 }
18 finally
19 {
20 // 下降写锁
21 _rwlock.DowngradeFromWriterLock(ref lc);
22 }
23 }
24 catch (ApplicationException)
25 {
26 Console.WriteLine("{0}thread upgrade reader lock failed!", thrdId);
27 }
28 }
29 finally
30 {
31 // 释放原来的读锁
32 _rwlock.ReleaseReaderLock();
33 }
34 }
35 catch (ApplicationException)
36 {
37 Console.WriteLine("{0}thread get reader lock out time!", thrdId);
38 }
39}
这里有一点要注意的就是读锁和写锁的超时等待时间间隔的设置,通常情况下设置写锁的等待超时要比读锁的长,否则会经常发生写锁等待失败的情况。
由于关于线程同步内容比较长,我将它分成两部分来写。下一篇我们将讨论利用内核对象进行线程同步。