.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操作是这样的

 1// 通监视器来创建临界区
 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>, 这里有一个小技巧,如果你想声明整个方法为线程同步可以使用方法属性
1// 通过属性设置整个方法为临界区
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时加入超时时限就可以。
1if (Monitor.TryEnter(Names))
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 关键字可将语句块标记为临界区,方法是获取给定对象的互斥锁,执行语句,然后释放该锁。
具体例子如下:
 1static public void ThreadFunc(object name)
 2{
 3     string str = name as string;
 4     Random rand = new Random();
 5     int count = rand.Next(100200);
 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来完成读锁和写锁控制的。

 1static public void ReaderThread(int thrdId)
 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来进行操作,而不是释放读锁。
 1static private void UpgradeAndDowngrade(int thrdId)
 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}

这里有一点要注意的就是读锁和写锁的超时等待时间间隔的设置,通常情况下设置写锁的等待超时要比读锁的长,否则会经常发生写锁等待失败的情况。

由于关于线程同步内容比较长,我将它分成两部分来写。下一篇我们将讨论利用内核对象进行线程同步
posted @ 2007-11-09 14:00  moonz-wu  阅读(4600)  评论(0编辑  收藏  举报