• 转载:详解lock,monitor,同步事件和等待句柄以及mutex


    对于引用类型和非线程安全的资源的同步处理,有四种相关处理:lock关键字,监视器(Monitor), 同步事件和等待句柄, mutex类。 
    Lock 关键字
        本人愚钝,在以前编程中遇到lock的问题总是使用lock(this)一锁了之,出问题后翻看MSDN突然发现下面几行字:通 常,应避免锁定 public 类型,否则实例将超出代码的控制范围。常见的结构 lock (this)、 lock (typeof (MyType)) 和 lock ("myLock") 违反此准则:如果实例可以被公共访问,将出 现 lock (this) 问题。如果 MyType 可以被公共访问,将出现 lock (typeof (MyType)) 问题。由于进程中使用 同一字符串的任何其他代码将共享同一个锁,所以出现 lock(“myLock”) 问题。来看看lock(this)的问题:如果有一个类 Class1,该类有一个方法用lock(this)来实现互斥:

    public void Method2()
            {
                
    lock (this)
                {
                    System.Windows.Forms.MessageBox.Show(
    "Method2 End");
                }
            } 



        如果在同一个Class1的实例中,该Method2能够互斥的执 行。但是如果是2个Class1的实例分别来执行Method2,是没有互斥效果的。因为这里的lock,只是对当前的实例对象进行了加锁。
        Lock(typeof(MyType)) 锁定住的对象范围更为广泛,由于一个类的所有实例都只有一个类型对象(该对象是typeof的返回结果),锁定它,就锁定了该对象的所有实例,微软现在建 议(原文请参考:http://www.microsoft.com/china/MSDN/library /enterprisedevelopment/softwaredev/SDaskgui06032003.mspx?mfr=true)不要使用 lock(typeof(MyType)),因为锁定类型对象是个很缓慢的过程,并且类中的其他线程、甚至在同一个应用程序域中运行的其他程序都可以访问 该类型对象,因此,它们就有可能代替您锁定类型对象,完全阻止您的执行,从而导致你自己的代码的挂起。
        锁住一个字符串更为神奇,只要字 符串内容相同,就能引起程序挂起。原因是在.NET中,字符串会被暂时存放,如果两个变量的字符串内容相同的话,.NET会把暂存的字符串对象分配给该变 量。所以如果有两个地方都在使用lock(“my lock”)的话,它们实际锁住的是同一个对象。到此,微软给出了个lock的建议用法:锁定一个私有 的static 成员变量。
          .NET在一些集合类中(比如ArrayList,HashTable,Queue,Stack)已经 提供了一个供lock使用的对象SyncRoot,用Reflector工具查看了SyncRoot属性的代码,在Array中,该属性只有一句 话:return this,这样和lock array的当前实例是一样的。ArrayList中的SyncRoot有所不同
     get
        {
            
    if (this._syncRoot == null)
            {
                Interlocked.CompareExchange(
    ref this._syncRoot, new object(), null);
            }
            
    return this._syncRoot; 

      这里要特别注意的是MSDN提到:从头到尾对一个集合进行枚举本质上并不是一个线程安全的过程。即使一个集合已进行同步,其他线程仍可以修改该集合, 这将导致枚举数引发异常。若要在枚举过程中保证线程安全,可以在整个枚举过程中锁定集合:

    Queue myCollection = new Queue();
      
    lock(myCollection.SyncRoot) {
      
    foreach (Object item in myCollection) {
      
    // Insert your code here.
      }
     } 


    Monitor类
    该类功效和 lock类似:


    System.Object obj 
    = (System.Object)x;
    System.Threading.Monitor.Enter(obj);
    try
    {
        DoSomething();
    }
    finally
    {
        System.Threading.Monitor.Exit(obj);


        lock关键字比Monitor简洁,其实lock就是对Monitor的Enter和Exit的一个封装。另外 Monitor还有几个常用的方法:TryEnter能够有效的决绝长期死等的问题,如果在一个并发经常发生,而且持续时间长的环境中使用 TryEnter,可以有效防止死锁或者长时间的等待。比如我们可以设置一个等待时间 bool gotLock = Monitor.TryEnter(myobject,1000),让当前线程在等待1000秒后根据返回的bool值来 决定是否继续下面的操作。Pulse以及PulseAll还有Wait方法是成对使用的,它们能让你更精确的控制线程之间的并发,MSDN关于这3个方法 的解释很含糊,有必要用一个具体的例子来说明一下:

    代码
    using System.Threading;
    public class Program {
       
    static object ball = new object();
       
    public static void Main() {
          Thread threadPing 
    = new Thread( ThreadPingProc );
          Thread threadPong 
    = new Thread( ThreadPongProc );
          threadPing.Start(); threadPong.Start();
          }
       
    static void ThreadPongProc() {
          System.Console.WriteLine(
    "ThreadPong: Hello!");
          
    lock ( ball )
             
    for (int i = 0; i < 5; i++){
                System.Console.WriteLine(
    "ThreadPong: Pong ");
                Monitor.Pulse( ball );
                Monitor.Wait( ball );
             }
          System.Console.WriteLine(
    "ThreadPong: Bye!");
       }
       
    static void ThreadPingProc() {
          System.Console.WriteLine(
    "ThreadPing: Hello!");
          
    lock ( ball )
             
    for(int i=0; i< 5; i++){
                System.Console.WriteLine(
    "ThreadPing: Ping ");
                Monitor.Pulse( ball );
                Monitor.Wait( ball );
             }
          System.Console.WriteLine(
    "ThreadPing: Bye!");
       }
    }



       其中Interlocked类是 专门为多个线程共享的变量提供原子操作(如果你想锁定的对象是基本数据类型,那么请使用这个类),CompareExchange方法将当前 syncRoot和null做比较,如果相等,就替换成new object(),这样做是为了保证多个线程在使用syncRoot时是线程安全的。集合 类中还有一个方法是和同步相关的:Synchronized,该方法返回一个对应的集合类的wrapper类,该类是线程安全的,因为他的大部分方法都用 lock来进行了同步处理,比如Add方法:
    public override void Add(object key, object value)
    {
        
    lock (this._table.SyncRoot)
        {
            
    this._table.Add(key, value);
        }

        执行结果如下(有可能是ThreadPong先执行):
    ThreadPing: Hello!
    ThreadPing: Ping
    ThreadPong: Hello!
    ThreadPong: Pong
    ThreadPing: Ping
    ThreadPong: Pong
    ThreadPing: Ping
    ThreadPong: Pong
    ThreadPing: Ping
    ThreadPong: Pong
    ThreadPing: Ping
    ThreadPong: Pong
    ThreadPing: Bye!

       当 threadPing进程进入ThreadPingProc锁定ball并调用Monitor.Pulse( ball );后,它通知 threadPong从阻塞队列进入准备队列,当threadPing调用Monitor.Wait( ball )阻塞自己后,它放弃了了对ball的 锁定,所以threadPong得以执行。PulseAll与Pulse方法类似,不过它是向所有在阻塞队列中的进程发送通知信号,如果只有一个线程被阻 塞,那么请使用Pulse方法。
    同步事件和等待句柄
       同步事件和等待句柄用于解决更复杂的同步情况,比如一个一个大的计算步骤包 含3个步骤result = first term + second term + third term,如果现在想写个多线程程序,同时计算 first term,second term 和third term,等所有3个步骤计算好后再把它们汇总起来,我们就需要使用到同步事件和等待句 柄,同步事件分有两个,分别为AutoResetEvent和ManualResetEvent,这两个类可以用来代表某个线程的运行状态:终止和非终 止,等待句柄用来判断ResetEvent的状态,如果是非终止状态就一直等待,否则放行,让等待句柄下面的代码继续运行。下面的代码示例阐释了如何使用 等待句柄来发送复杂数字计算的不同阶段的完成信号。此计算的格式 为:result = first term + second term + third term

    using System;
    using System.Threading;
    class CalculateTest
    {
        
    static void Main()
        {
            Calculate calc 
    = new Calculate();
            Console.WriteLine(
    "Result = {0}."
                calc.Result(
    234).ToString());
            Console.WriteLine(
    "Result = {0}."
                calc.Result(
    55).ToString());
        }
    }
    class Calculate
    {
        
    double baseNumber, firstTerm, secondTerm, thirdTerm;
        AutoResetEvent[] autoEvents;
        ManualResetEvent manualEvent;
        
    // Generate random numbers to simulate the actual calculations.
        Random randomGenerator;
        
    public Calculate()
        {
            autoEvents 
    = new AutoResetEvent[]
            {
                
    new AutoResetEvent(false),
                
    new AutoResetEvent(false),
                
    new AutoResetEvent(false)
            };
            manualEvent 
    = new ManualResetEvent(false);
        }
        
    void CalculateBase(object stateInfo)
        {
            baseNumber 
    = randomGenerator.NextDouble();

            
    // Signal that baseNumber is ready.
            manualEvent.Set();
        }
        
    // The following CalculateX methods all perform the same
        
    // series of steps as commented in CalculateFirstTerm.
     void CalculateFirstTerm(object stateInfo)
        {
            
    // Perform a precalculation.
            double preCalc = randomGenerator.NextDouble();
            
    // Wait for baseNumber to be calculated.
            manualEvent.WaitOne();
            
    // Calculate the first term from preCalc and baseNumber.
            firstTerm = preCalc * baseNumber * 
                randomGenerator.NextDouble();
            
    // Signal that the calculation is finished.
            autoEvents[0].Set();
        }
        
    void CalculateSecondTerm(object stateInfo)
        {
            
    double preCalc = randomGenerator.NextDouble();
            manualEvent.WaitOne();
            secondTerm 
    = preCalc * baseNumber * 
                randomGenerator.NextDouble();
            autoEvents[
    1].Set();
        }
        
    void CalculateThirdTerm(object stateInfo)
        {
            
    double preCalc = randomGenerator.NextDouble();
            manualEvent.WaitOne();
            thirdTerm 
    = preCalc * baseNumber * 
                randomGenerator.NextDouble();
            autoEvents[
    2].Set();
        }
        
    public double Result(int seed)
        {
            randomGenerator 
    = new Random(seed);

            
    // Simultaneously calculate the terms.
            ThreadPool.QueueUserWorkItem(
                
    new WaitCallback(CalculateBase));
            ThreadPool.QueueUserWorkItem(
                
    new WaitCallback(CalculateFirstTerm));
            ThreadPool.QueueUserWorkItem(
                
    new WaitCallback(CalculateSecondTerm));
            ThreadPool.QueueUserWorkItem(
                
    new WaitCallback(CalculateThirdTerm));
            
    // Wait for all of the terms to be calculated.
            WaitHandle.WaitAll(autoEvents);
            
    // Reset the wait handle for the next calculation.
            manualEvent.Reset();
            
    return firstTerm + secondTerm + thirdTerm;
        }


       该示例一共有4个ResetEvent,一个ManualEvent,三个 AutoResetEvent,分别反映4个线程的运行状态。ManualEvent和AutoResetEvent有一点不 同:AutoResetEvent是在当前线程调用set方法激活某线程之后,AutoResetEvent状态自动重置,而ManualEvent则需 要手动调用Reset方法来重置状态。接着来看看上面那段代码的执行顺序,Main方法首先调用的是Result 方法,Result方法开启4个线程分 别去执行,主线程阻塞在WaitHandle.WaitAll(autoEvents)处,等待3个计算步骤的完成。4个ResetEvent初始化状态 都是非终止(构造实例时传入了false),CalculateBase首先执行完毕,其他3个线程阻塞在manualEvent.WaitOne() 处,等待CalculateBase执行完成。CalculateBase生成baseNumber后,把代表自己的ManualEvent状态设置为终 止状态。其他几个线程从manualEvent.WaitOne()处恢复执行,在执行完自己的代码后把自己对应的AutoResetEvent状态置为 终止。当3个计算步骤执行完后,主线程从阻塞中恢复,把三个计算结果累加后返回。还要多补充一点的是WaitHandle的 WaitOne,WaitAll,WaitAny方法,如果等待多个进程就用WaitAll,如本例中 的:WaitHandle.WaitAll(autoEvents),WaitAny是等待的线程中有一个结束则停止等待。
    Mutex 对象

        Mutex 与Monitor类似,这里不再累赘,需要注意的是Mutex分两种:一种是本地Mutex一种是系统级Mutex,系统级Mutex可以用来进行跨进程 间的线程的同步。尽管 mutex 可以用于进程内的线程同步,但是使用 Monitor 通常更为可取,因为监视器是专门 为 .NET Framework 而设计的,因而它可以更好地利用资源。相比之下,Mutex 类是 Win32 构造的包装。尽管 mutex 比监 视器更为强大,但是相对于 Monitor 类,它所需要的互操作转换更消耗计算资源。

    from:http://www.diybl.com/course/4_webprogram/asp.net/asp_netshl/2008227/102020.html

  • 相关阅读:
    马士兵_聊天系统_知识储备库
    最近一些情况
    JAVA坦克大战项目练习日4
    JAVA坦克大战项目练习日3
    JAVA坦克大战项目练习日2
    JAVA坦克大战项目练习日1
    课后实战String的一些细节
    Java从入门到精通之数组篇
    58_自定义标签打包以及使用自定义标签教程
    javaWeb安全篇(1)——防盗链技术的实现
  • 原文地址:https://www.cnblogs.com/wuming/p/1675838.html
Copyright © 2020-2023  润新知