多线程:C#线程同步lock,Monitor,Mutex,同步事件和等待句柄(上)

C#线程同步机制
本文介绍了C#中的线程同步方法,包括lock关键字、Monitor类及其相关方法的使用。详细解析了lock关键字背后的实现原理及Monitor类的Wait和Pulse方法在生产者消费者模式中的应用。

转自 http://www.cnblogs.com/freshman0216/archive/2008/07/27/1252253.html

  本篇从Monitor,Mutex,ManualResetEvent,AutoResetEvent,WaitHandler的类关系图开 始,希望通过本篇的介绍能对常见的线程同步方法有一个整体的认识,而对每种方式的使用细节,适用场合不会过多解释。让我们来看看这几个类的关系图:

 

      1.lock关键字

      lock是C#关键词,它将语句块标记为临界区,确保当一个线程位于代码的临界区时,另一个线程不进入临界区。如果其他线程试图进入锁定的代码,则它将一直等待(即被阻止),直到该对象被释放。方法是获取给定对象的互斥锁,执行语句,然后释放该锁。

      MSDN上给出了使用lock时的注意事项通常,应避免锁定 public 类型,否则实例将超出代码的控制范围。常见的结构 lock (this)、lock (typeof (MyType)) 和 lock ("myLock") 违反此准则。

      1)如果实例可以被公共访问,将出现 lock (this) 问题。

      2)如果 MyType 可以被公共访问,将出现 lock (typeof (MyType)) 问题由于一个类的所有实例都只有一个类型对象(该对象是typeof的返回结果),锁定它,就锁定了该对象的所有实例。微软现在建议不要使用 lock(typeof(MyType)),因为锁定类型对象是个很缓慢的过程,并且类中的其他线程、甚至在同一个应用程序域中运行的其他程序都可以访问 该类型对象,因此,它们就有可能代替您锁定类型对象,完全阻止您的执行,从而导致你自己的代码的挂起。

      3)由于进程中使用同一字符串的任何其他代码将共享同一个锁,所以出现 lock(“myLock”) 问题。这个问题和.NET Framework创建字符串的机制有关系,如果两个string变量值都是"myLock",在内存中会指向同一字符串对象。

      最佳做法是定义 private 对象来锁定, 或 private static对象变量来保护所有实例所共有的数据。

      我们再来通过IL Dasm看看lock关键字的本质,下面是一段简单的测试代码:

     lock  (lockobject)
ExpandedBlockStart.gifContractedBlock.gif    
{
        
int i = 5;
    }

      用IL Dasm打开编译后的文件,上面的语句块生成的IL代码为:

       IL_0045:    call        void [mscorlib]System.Threading. Monitor :: Enter (object)
      
IL_004a:    nop
      .try
      {
        
IL_004b:    nop
        
IL_004c:   ldc.i4. 5
        
IL_004d:   stloc. 1
        
IL_004e:    nop
        
IL_004f:    leave .s    IL_0059
      }  // end .try
      finally
      {
        
IL_0051:   ldloc. 3
        
IL_0052:    call        void [mscorlib]System.Threading. Monitor ::Exit(object)
        
IL_0057:    nop
        
IL_0058:   endfinally
      }  // end handler

      通过上面的代码我们很清楚的看到:lock关键字其实就是对Monitor类的Enter()和Exit()方法的封装,并通过try...catch...finally语句块确保在lock语句块结束后执行Monitor.Exit()方法,释放互斥锁。

      2.Monitor类

      Monitor类通过向单个线程授予对象锁来控制对对象的访问。对象锁提供限制访问临界区的能力。当一个线程拥有对象的锁时,其他任何 线程都不能获取该锁。还可以使用 Monitor 来确保不会允许其他任何线程访问正在由锁的所有者执行的应用程序代码节,除非另一个线程正在使用其他的锁定对象执行该代码。

      通过对lock关键字的分析我们知道,lock就是对Monitor的Enter和Exit的一个封装,而且使用起来更简洁,因此Monitor类的Enter()和Exit()方法的组合使用可以用lock关键字替代。

      另外Monitor类还有几个常用的方法:

      TryEnter()能够有效的解决长期死等的问题,如果在一个并发经常发生,而且持续时间长的环境中使用TryEnter,可以有效防止死锁或者长时间 的等待。比如我们可以设置一个等待时间bool gotLock = Monitor.TryEnter(myobject,1000),让当前线程在等待1000秒后根据返回的bool值来决定是否继续下面的操作。

      Wait()释放对象上的锁以便允许其他线程锁定和访问该对象。在其他线程访问对象时,调用线程将等待。脉冲信号用于通知等待线程有关对象状态的更改。

      Pulse(),PulseAll()向一个或多个等待线程发送信号。该信号通知等待线程锁定对象的状态已更改,并且锁的所有者准备释放该锁。等待线程被 放置在对象的就绪队列中以便它可以最后接收对象锁。一旦线程拥有了锁,它就可以检查对象的新状态以查看是否达到所需状态。

      注意:Pulse、PulseAll和Wait方法必须从同步的代码块内调用。

      我们假定一种情景:妈妈做蛋糕,小孩有点馋,妈妈每做好一块就要吃掉,妈妈做好一块后,告诉小孩蛋糕已经做好了。下面的例子用Monitor类的Wait和Pulse方法模拟小孩吃蛋糕的情景。

ContractedBlock.gif ExpandedBlockStart.gif
    //仅仅是说明Wait和Pulse/PulseAll的例子
    
//逻辑上并不严密,使用场景也并不一定合适
    class MonitorSample
    {
        
private int n = 1;  //生产者和消费者共同处理的数据
        private int max = 10000;

        
private object monitor = new object();

        
public void Produce()
        {
            
lock (monitor)
            {
                
for (; n <= max; n++)
                {
                    Console.WriteLine(
"妈妈:第" + n.ToString() + "块蛋糕做好了");
                    
//Pulse方法不用调用是因为另一个线程中用的是Wait(object,int)方法
                    
//该方法使被阻止线程进入了同步对象的就绪队列
                    
//是否需要脉冲激活是Wait方法一个参数和两个参数的重要区别
                    
//Monitor.Pulse(monitor);
                    
//调用Wait方法释放对象上的锁并阻止该线程(线程状态为WaitSleepJoin)
                    
//该线程进入到同步对象的等待队列,直到其它线程调用Pulse使该线程进入到就绪队列中
                    
//线程进入到就绪队列中才有条件争夺同步对象的所有权
                    
//如果没有其它线程调用Pulse/PulseAll方法,该线程不可能被执行
                    Monitor.Wait(monitor);
                }
            }
        }

        
public void Consume()
        {
            
lock (monitor)
            {
                
while (true)
                {
                    
//通知等待队列中的线程锁定对象状态的更改,但不会释放锁
                    
//接收到Pulse脉冲后,线程从同步对象的等待队列移动到就绪队列中
                    
//注意:最终能获得锁的线程并不一定是得到Pulse脉冲的线程
                    Monitor.Pulse(monitor);
                    
//释放对象上的锁并阻止当前线程,直到它重新获取该锁
                    
//如果指定的超时间隔已过,则线程进入就绪队列
                    Monitor.Wait(monitor,1000);
                    Console.WriteLine(
"孩子:开始吃第" + n.ToString() + "块蛋糕");
                }
            }
        }

        
static void Main(string[] args)
        {
            MonitorSample obj 
= new MonitorSample();
            Thread tProduce 
= new Thread(new ThreadStart(obj.Produce));
            Thread tConsume 
= new Thread(new ThreadStart(obj.Consume));
            
//Start threads.
            tProduce.Start();
            tConsume.Start();

            Console.ReadLine();
        }
    }

      这个例子的目的是要理解Wait和Pulse如何保证线程同步的,同时要注意Wait(obeject)和Wait(object,int)方法的区别,理解它们的区别很关键的一点是要理解同步的对象包含若干引用,其中包括对当前拥有锁的线程的引用、对就绪队列(包含准备获取锁的线程)的引用和对等待队列(包含等待对象状态更改通知的线程)的引用。

 
1.几种同步方法的区别 lockMonitor.NET用一个特殊结构实现的,Monitor对象是完全托管的、完全可移植的,并且在操作系统资源要求方 面可能更为有效,同步速度较快,但不能跨进程同步lockMonitor.EnterMonitor.Exit方法的封装),主要作用是锁定临界区,使临 界区代码只能被获得锁的线程执行。Monitor.WaitMonitor.Pulse用于线程同步,类似信号操作,个人感觉使用比较复杂,容易造成死 锁。 互斥体Mutex事件对象EventWaitHandler属于内核对象,利用内核对象进行线程同步线程必须要在用户模式内核模 式间切换,所以一般效率很低,但利用互斥对象事件对象这样的内核对象,可以在多个进程中的各个线程间进行同步。 互斥体Mutex类似于一个接力棒,拿到接力棒的线程才可以开始跑,当然接力棒一次只属于一个线程(Thread Affinity),如果这个线程不释放接力棒(Mutex.ReleaseMutex),那么没办法,其他所有需要接力棒运行的线程都知道能等着看热 闹。 EventWaitHandle 类允许线程通过发信号互相通信。 通常,一个或多个线程在 EventWaitHandle 上阻止,直到一个未阻止的线程调用 Set 方法,以释放一个或多个被阻止的线程。 2.什么时候需要锁定 首先要理解锁定是解决竞争条件的,也就是多个线程同时访问某个资源,造成意想不到的结果。比如,最简单的情况是,一个计数器,两个线程 同时加一,后果就是损失了一个计数,但相当频繁的锁定又可能带来性能上的消耗,还有最可怕的情况死锁。那么什么情况下我们需要使用锁,什么情况下不需要 呢? 1)只有共享资源才需要锁定 只有可以被多线程访问的共享资源才需要考虑锁定,比如静态变量,再比如某些缓存中的值,而属于线程内部的变量不需要锁定。 2)多使用lock,少用Mutex 如果你一定要使用锁定,请尽量不要使用内核模块的锁定机制,比如.NET的Mutex,Semaphore,AutoResetEvent ManuResetEvent,使用这样的机制涉及到了系统在用户模式内核模式间的切换,性能差很多,但是他们的优点是可以跨进程同步线程,所以应该清 楚的了解到他们的不同适用范围。 3)了解你的程序是怎么运行的 实际上在web开发中大多数逻辑都是在单个线程中展开的,一个请求都会在一个单独的线程中处理,其中的大部分变量都是属于这个线程的,根本没有必要考虑锁 定,当然对于ASP.NET中的Application对象中的数据,我们就要考虑加锁了。 4)把锁定交给数据库 数 据库除了存储数据之外,还有一个重要的用途就是同步,数据库本身用了一套复杂的机制来保证数据的可靠一致性,这就为我们节省了很多的精力。保证了数据源 头上的同步,我们多数的精力就可以集中在缓存等其他一些资源的同步访问上了。通常,只有涉及到多个线程修改数据库中同一条记录时,我们才考虑加锁。 5)业务逻辑对事务线程安全的要求 这 条是最根本的东西,开发完全线程安全的程序是件很费时费力的事情,在电子商务等涉及金融系统的案例中,许多逻辑都必须严格的线程安全,所以我们不得不牺牲 一些性能,很多的开发时间来做这方面的工作。而一般的应用中,许多情况下虽然程序有竞争的危险,我们还是可以不使用锁定,比如有的时候计数器少一多一, 对结果无伤大雅的情况下,我们就可以不用去管它。 3.InterLocked类 Interlocked 类提供了同步对多个线程共享的变量的访问的方法。如果该变量位于共享内存中,则不同进程的线程就可以使用该机制。互锁操作是原子的,即整个操作是不能由相 同变量上的另一个互锁操作所中断的单元。这在抢先多线程操作系统中是很重要的,在这样的操作系统中,线程可以在从某个内存地址加载值之后但是在有机会更改 存储该值之前被挂起。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值