.NET多线程同步方法详解

    本文主要描述在C#中线程同步的方法。线程的基本概念网上资料也很多就不再赘述了。直接接入主题,在多线程开发的应用中,线程同步是不可避免的。在.Net框架中,实现线程同步主要通过以下的几种方式来实现,在MSDN的线程指南中已经讲了几种,本文结合作者实际中用到的方式一起说明一下。 

1. 维护自由锁(InterLocked)实现同步 
2. 监视器(Monitor)和互斥锁(lock) 
3. 读写锁(ReadWriteLock) 
4. 系统内核对象 
1) 互斥(Mutex), 信号量(Semaphore), 事件(AutoResetEvent/ManualResetEvent) 
2) 线程池 

   除了以上的这些对象之外实现线程同步的还可以使用Thread.Join方法。这种方法比较简单,当你在第一个线程运行时想等待第二个线程执行结果,那么你可以让第二个线程Join进来就可以了。 

自由锁(InterLocked) 

    对一个32位的整型数进行递增和递减操作来实现锁,有人会问为什么不用++或--来操作。因为在多线程中对锁进行操作必须是原子的,而++和--不具备这个能力。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操作是这样的:
 

         //  通监视器来创建临界区 
         static   public   void  DelUser( string  name)
        {
             try
            {
                 //  等待线程进入 
                Monitor.Enter(Names);
                Names.Remove(name);
                Console.WriteLine( " Del: {0} " , Names.Count);
                Monitor.Pulse(Names);
            }
             finally
            {
                 //  释放对象锁 
                Monitor.Exit(Names);
            }
        } 
    }

   其中Names是一个List<string>, 这里有一个小技巧,如果你想声明整个方法为线程同步可以使用方法属性:

         //  通过属性设置整个方法为临界区 
        [MethodImpl(MethodImplOptions.Synchronized)] 
         static   public   void  AddUser( string  name) 
        { 
            Names.Add(name); 
            Console.WriteLine( " Add: {0} " ,Names.Count); 
        }

    对于Monitor的使用有一个方法是比较诡异的,那就是Wait方法。在MSDN中对Wait的描述是: 释放对象上的锁以便允许其他线程锁定和访问该对象。 

    这里提到的是先释放锁,那么显然我们需要先得到锁,否则调用Wait会出现异常,所以我们必须在Wait前面调用Enter方法或其他获取锁的方法,如lock,这点很重要。对应Enter方法,Monitor给出来另一种实现TryEnter。这两种方法的主要区别在于是否阻塞当前线程,Enter方法在获取不到锁时,会阻塞当前线程直到得到锁。不过缺点是如果永远得不到锁那么程序就会进入死锁状态。我们可以采用Wait来解决,在调用Wait时加入超时时限就可以。

             if  (Monitor.TryEnter(Names))
            {
                Monitor.Wait(Names,  1000 );  //  !! 
                Names.Remove(name); 
                Console.WriteLine( " Del: {0} " , Names.Count);
                Monitor.Pulse(Names); 
            }  

互斥锁(lock)

 lock关键字是实现线程同步的比较简单的方式,其实就是设置一个临界区。在lock之后的{...}区块为一个临界区,当进入临界区时加互斥锁,离开临界区时释放互斥锁。MSDN对lock关键字的描述是: lock 关键字可将语句块标记为临界区,方法是获取给定对象的互斥锁,执行语句,然后释放该锁。 
具体例子如下:

         static   public   void  ThreadFunc( object  name)
        {
             string  str  =  name  as   string ;
            Random rand  =   new  Random();
             int  count  =  rand.Next( 100 ,  200 );
             for  ( int  i  =   0 ; i  <  count; i ++ )
            {
                 lock  (NumList)
                {
                    NumList.Add(i);
                    Console.WriteLine( " {0} {1} " , str, i);
                }
            }
        }

   对lock的使用有几点建议:对实例锁定lock(this),对静态变量锁定lock(typeof(val))。lock的对象访问权限最好是private,否则会出现失去访问控制现象。

读写锁(ReadWriteLock)

读写锁的出现主要是在很多情况下,我们读资源的操作要多于写资源的操作。但是如果每次只对资源赋予一个线程的访问权限显然是低效的,读写锁的优势是同时可以有多个线程对同一资源进行读操作。因此在读操作比写操作多很多,并且写操作的时间很短的情况下使用读写锁是比较有效率的。读写锁是一个非静态类所以你在使用前需要先声明一个读写锁对象: 
static   private  ReaderWriterLock _rwlock  =   new  ReaderWriterLock(); 

    读写锁是通过调用AcquireReaderLock,ReleaseReaderLock,AcquireWriterLock,ReleaseWriterLock来完成读锁和写锁控制的 

         static   public   void  ReaderThread( int  thrdId) 
        { 
             try  
            {  //  请求读锁,如果100ms超时退出 
                _rwlock.AcquireReaderLock( 10 ); 
                 try  
                { 
                     int  inx  =  _rand.Next(_list.Count); 
                     if  (inx  <  _list.Count) 
                        Console.WriteLine( " {0}thread {1} " , thrdId, _list[inx]); 
                } 
                 finally  
                {
                    _rwlock.ReleaseReaderLock(); 
                } 
            } 
             catch  (ApplicationException)  //  如果请求读锁失败 
            { 
                Console.WriteLine( " {0}thread get reader lock out time! " , thrdId); 
            } 
        } 
         static   public   void  WriterThread() 
        { 
             try  
            {
                 //  请求写锁 
                _rwlock.AcquireWriterLock( 100 ); 
                 try  
                { 
                     string  val  =  _rand.Next( 200 ).ToString(); 
                    _list.Add(val);  //  写入资源 
                    Console.WriteLine( " writer thread has written {0} " , val); 
                } 
                 finally  
                {  //  释放写锁 
                    _rwlock.ReleaseWriterLock(); 
                } 
            } 
             catch  (ApplicationException) 
            { 
                Console.WriteLine( " Get writer thread lock out time! " ); 
            } 
        } 

    如果你想在读的时候插入写操作请使用UpgradeToWriterLock和DowngradeFromWriterLock来进行操作,而不是释放读锁。

         static   private   void  UpgradeAndDowngrade( int  thrdId) 
        { 
             try  
            { 
                _rwlock.AcquireReaderLock( 10 ); 
                 try  
                { 
                     try  
                    {
                         //  提升读锁到写锁 
                        LockCookie lc  =  _rwlock.UpgradeToWriterLock( 100 );
                         try
                        {
                             string  val  =  _rand.Next( 500 ).ToString();
                            _list.Add(val); Console.WriteLine( " Upgrade Thread{0} add {1} " , thrdId, val); 
                        } 
                         finally
                        {  //  下降写锁 
                            _rwlock.DowngradeFromWriterLock( ref  lc); 
                        } 
                    } 
                     catch  (ApplicationException)
                    { 
                        Console.WriteLine( " {0}thread upgrade reader lock failed! " , thrdId); 
                    } 
                } 
                 finally  
                {
                     //  释放原来的读锁 
                    _rwlock.ReleaseReaderLock();
                }
            } 
             catch  (ApplicationException) 
            { 
                Console.WriteLine( " {0}thread get reader lock out time! " , thrdId);
            }
        }

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

系统内核对象 互斥对象(Mutex) 

    互斥对象的作用有点类似于监视器对象,确保一个代码块在同一时刻只有一个线程在执行。互斥对象和监视器对象的主要区别就是,互斥对象一般用于跨进程间的线程同步,而监视器对象则用于进程内的线程同步。互斥对象有两种:一种是命名互斥;另一种是匿名互斥。在跨进程中使用到的就是命名互斥,一个已命名的互斥就是一个系统级的互斥,它可以被其他进程所使用,只要在创建互斥时指定打开互斥的名称就可以。在.Net中互斥是通过Mutex类来实现。 

  其实对于OpenExisting函数有两个重载版本, 
   Mutex.OpenExisting (String) 
   Mutex.OpenExisting (String, MutexRights) 
   对于默认的第一个函数其实是实现了第二个函数 MutexRights.Synchronize|MutexRights.Modify操作。

    由于监视器的设计是基于.Net框架,而Mutex类是系统内核对象封装了win32的一个内核结构来实现互斥,并且互斥操作需要请求中断来完成,因此在进行进程内线程同步的时候性能上要比互斥要好。 

    典型的使用Mutex同步需要完成三个步骤的操作:1.打开或者创建一个Mutex实例;2.调用WaitOne()来请求互斥对象;3.最后调用ReleaseMutex来释放互斥对象。

         static   public   void  AddString( string  str) 
        {
             //  设置超时时限并在wait前退出非默认托管上下文 
             if  (_mtx.WaitOne( 1000 ,  true ))
            {
                _resource.Add(str); 
                _mtx.ReleaseMutex(); 
            } 
        } 

   需要注意的是,WaitOne和ReleaseMutex必须成对出现,否则会导致进程死锁的发生,这时系统(.Net2.0)框架会抛出AbandonedMutexException异常。 

信号量(Semaphore)  

   信号量就像一个夜总会:它有确切的容量,并被保镖控制。一旦满员,就没有人能再进入,其他人必须在外面排队。那么在里面离开一个人后,队头的人就可以进入。信号量的构造函数需要提供至少两个参数-现有的人数和最大的人数。 
信号量的行为有点类似于Mutex或是lock,但是信号量没有拥有者。任意线程都可以调用Release来释放信号量而不像Mutex和lock那样需要线程得到资源才能释放。 
     class  SemaphoreTest 
    {
         static  Semaphore s  =   new  Semaphore( 3 ,  3 );  //  当前值=3; 容量=3 
         static   void  Main() 
        {
             for  ( int  i  =   0 ; i  <   10 ; i ++ ) 
                 new  Thread(Go).Start(); 
        } 
         static   void  Go()
        {
             while  ( true )
            {
                s.WaitOne();
                Thread.Sleep( 100 );  //  一次只有个线程能被处理 
                s.Release(); 
            } 
        }
    } 
 

事件(ManualResetEvent/AutoResetEvent)    < src="http://blog.csdn.net/count.aspx?ID=1857459&Type=Rank" type="text/javascript">
AutoResetEvent 

    一个AutoResetEvent象是一个"检票轮盘":插入一张通行证然后让一个人通过。"auto"的意思就是这个"轮盘"自动关闭或者打开让某人通过。线程将在调用WaitOne后进行等待或者是阻塞,并且通过调用Set操作来插入线程。如果一堆线程调用了WaitOne操作,那么"轮盘"就会建立一个等待队列。一个通行证可以来自任意一个线程,换句话说任意一个线程都可以通过访问AutoResetEvent对象并调用Set来释放一个阻塞的线程。 

    如果在Set被调用的时候没有线程等待,那么句柄就会一直处于打开状态直到有线程调用了WaitOne操作。这种行为避免了竞争条件-当一个线程还没来得急释放而另一个线程就开始进入的情况。因此重复的调用Set操作一个"轮盘"哪怕是没有等待线程也不会一次性的让所有线程进入。

    WaitOne操作接受一个超时参数-当发生等待超时的时候,这个方法会返回一个false。当已有一个线程在等待的时候,WaitOne操作可以指定等待还是退出当前同步上下文。Reset操作提供了关闭"轮盘"的操作。AutoResetEvent能够通过两个方法来创建: 1.调用构造函数 EventWaitHandle wh = new AutoResetEvent (false); 如果boolean值为true,那么句柄的Set操作将在创建后自动被调用 ;2. 通过基类EventWaitHandle方式 EventWaitHandle wh = new EventWaitHandle (false, EventResetMode.Auto); EventWaitHandle构造函数允许创建一个ManualResetEvent。人们应该通过调用Close来释放一个Wait Handle在它不再使用的时候。当在应用程序的生存期内Wait handle继续被使用,那么如果遗漏了Close这步,在应用程序关闭的时候也会被自动释放。 
     class  BasicWaitHandle 
    {
         static  EventWaitHandle wh  =   new  AutoResetEvent( false );
         static   void  Main()
        {
             new  Thread(Waiter).Start();
            Thread.Sleep( 1000 );  //  等待一会儿 
            wh.Set();  //  唤醒 
        } 
         static   void  Waiter()
        {
            Console.WriteLine( " Waiting... " );
            wh.WaitOne();  //  等待唤醒 
            Console.WriteLine( " Notified " ); 
        }
    } 

ManualResetEvent 

    ManualResetEvent是AutoResetEvent的一个特例。它的不同之处在于在线程调用WaitOne后不会自动的重置状态。它的工作机制有点象是开关:调用Set打开并允许其他线程进行WaitOne;调用Reset关闭那么排队的线程就要等待,直到下一次打开。可以使用一个带volatile声明的boolean字段来模拟间断休眠 - 通过重复检测标志,然后休眠一小段时间。

    ManualResetEvent常常被用于协助完成一个特殊的操作,或者让一个线程在开始工作前完成初始化。

 

 线程池(Thread Pooling)  

    如果你的应用程序拥有大量的线程并花费大量的时间阻塞在一个Wait Handle上,那么你要考虑使用线程池(Thead pooling)来处理。线程池通过合并多个Wait Handle来节约等待的时间。当Wait Handle被激活时,使用线程池你需要注册一个Wait Handle到一个委托去执行。通过调用ThreadPool.RegisterWaitForSingleObject方法:

     class  Test 
    {
         static  ManualResetEvent starter  =   new  ManualResetEvent( false ); 
         public   static   void  Main()
        {
            ThreadPool.RegisterWaitForSingleObject(starter, Go,  " hello " ,  - 1 ,  true );
            Thread.Sleep( 5000 );
            Console.WriteLine( " Signaling worker... " );
            starter.Set(); 
            Console.ReadLine();
        }
         public   static   void  Go( object  data,  bool  timedOut) 
        {
            Console.WriteLine( " Started  "   +  data);  //  Perform task... 
        }
    } 

    对于Wait Handle和委托,RegisterWaitForSingleObject接受一个"黑盒"对象并传递给你的委托(就像ParameterizedThreadStart),超时设置和boolean标志指示了关闭和循环的请求。所有进入池中的线程都被认为是后台线程,这就意味着它们不再由应用程序控制,而是由系统控制直到应用程序退出。 

    注意:如果这时候调用Abort操作,可能会发生意想不到的情况。 
   
   你也可以通过调用QueueUserWorkItem方法使用线程池,指定委托并立即被执行。这时你不能在多任务情况下保存共享线程,但是可以得到另外的好处:线程池会保持一个线程的总容量,当作业数超出容量时自动插入任务。

     class  Test 
    {
         static   object  workerLocker  =   new   object ();
         static   int  runningWorkers  =   100 ;
         public   static   void  Main() 
        {
             for  ( int  i  =   0 ; i  <  runningWorkers; i ++ ) 
            {
                ThreadPool.QueueUserWorkItem(Go, i); 
            }
            Console.WriteLine( " Waiting for threads to complete... " ); 
             lock  (workerLocker) 
            {
                 while  (runningWorkers  >   0 ) 
                    Monitor.Wait(workerLocker);
            }
            Console.WriteLine( " Complete! " );
            Console.ReadLine(); 
        }
         public   static   void  Go( object  instance) 
        {
            Console.WriteLine( " Started:  "   +  instance);
            Thread.Sleep( 1000 ); 
            Console.WriteLine( " Ended:  "   +  instance); 
             lock  (workerLocker)
            {
                runningWorkers -- ;
                Monitor.Pulse(workerLocker);
            }
        }
    } 

  为了传递多个对象到目标方法,你必须定义一个客户对象并包含所有属性或通过调用异步的委托。如Go方法接受两参数:

ThreadPool.QueueUserWorkItem ( delegate  ( object  notUsed) { Go ( 23 , 34 ); }); 

 其他的方法可以使用异步委托。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值