多线程同步与并发访问共享资源工具—Lock、Monitor、Mutex、Semaphore

“线程同步”的含义


        当一个进程启动了多个线程时,如果需要控制这些线程的推进顺序(比如A线程必须等待B和C线程执行完毕之后才能继续执行),则称这些线程需要进行“线程同步(thread synchronization)”。

        线程同步的道理虽然简单,但却是给多线程开发带来复杂性的根源之一。当线程同步不好时,有可能会出现一种特殊的情形——死锁(Dead Lock)


“死锁”的含义


        死锁表示系统进入了一个僵化状态,所有线程都没有执行完毕,但却谁也没法继续执行。究其根源,是因为“进程推进顺序不当”和“资源共享”。如例:

        1)进程推进顺序不当造成死锁

[csharp]  view plain copy print ?
  1. using System;  
  2. using System.Collections.Generic;  
  3. using System.Text;  
  4. using System.Threading;  
  5.   
  6. namespace JoinLeadToDeadlock  
  7. {  
  8.     class Program  
  9.     {  
  10.         static Thread mainThread;  
  11.         static void Main(string[] args)  
  12.         {  
  13.             Console.WriteLine("主线程开始运行");  
  14.             mainThread = Thread.CurrentThread;  
  15.   
  16.             Thread ta = new Thread(new ThreadStart(ThreadAMethod));  
  17.             ta.Start();  //线程A开始执行  
  18.             Console.WriteLine("主线程等待线程A结束……");  
  19.             ta.Join();    //等待线程A结束  
  20.             Console.WriteLine("主线程退出");  
  21.         }  
  22.   
  23.         static void ThreadAMethod()  
  24.         {  
  25.             for (int i = 0; i < 10; i++)  
  26.             {  
  27.                 Console.WriteLine(Convert.ToString(i) + ": 线程A正在执行");  
  28.                 Thread.Sleep(1000);  
  29.             }  
  30.             Console.WriteLine("线程A等待主线程退出……");  
  31.             mainThread.Join();  //等待主线程结束  
  32.         }  
  33.     }  
  34. }  
        在该例中,主线程mainThread先开始执行,然后启动线程ta,线程ta执行结束前又要等待mainThread线程执行结束,这样就出现了“交叉等待”的局面,必然死锁!


        2)共享资源造成死锁

        所谓“共享资源”,指的是多个线程可以同时访问的数据结构、文件等信息实体。

[csharp]  view plain copy print ?
  1. using System;  
  2. using System.Collections.Generic;  
  3. using System.Text;  
  4. using System.Threading;  
  5.   
  6.   
  7. namespace SharedResourceLeadToDeadlock  
  8. {  
  9.     class Program  
  10.     {  
  11.         //共享资源  
  12.         static SharedResource R1 = new SharedResource();  
  13.         static SharedResource R2 = new SharedResource();  
  14.           
  15.         static void Main(string[] args)  
  16.         {  
  17.             Thread th1 = new Thread(UseSharedResource1);  
  18.             Thread th2 = new Thread(UseSharedResource2);  
  19.             th1.Start();  
  20.             th2.Start();  
  21.             //等待两线程运行结束  
  22.             th1.Join();  
  23.             th2.Join();  
  24.         }  
  25.   
  26.         static void UseSharedResource1()  
  27.         {  
  28.             System.Console.WriteLine("线程{0}申请使用资源R1", Thread.CurrentThread.ManagedThreadId);  
  29.             Monitor.Enter(R1);  //对R1加锁  
  30.             System.Console.WriteLine("线程{0}独占使用资源R1", Thread.CurrentThread.ManagedThreadId);  
  31.             Thread.Sleep(1000);  
  32.             System.Console.WriteLine("线程{0}申请使用资源R2", Thread.CurrentThread.ManagedThreadId);  
  33.             Monitor.Enter(R2);  //对R2加锁  
  34.             System.Console.WriteLine("线程{0}独占使用资源R2", Thread.CurrentThread.ManagedThreadId);  
  35.             Thread.Sleep(1000);  
  36.             System.Console.WriteLine("线程{0}资源R2使用完毕,放弃", Thread.CurrentThread.ManagedThreadId);  
  37.             Monitor.Exit(R2);   //对R2解锁  
  38.             System.Console.WriteLine("线程{0}资源R1使用完毕,放弃", Thread.CurrentThread.ManagedThreadId);  
  39.             Monitor.Exit(R1);  //对R1解锁  
  40.         }  
  41.   
  42.         static void UseSharedResource2()  
  43.         {  
  44.             System.Console.WriteLine("线程{0}申请使用资源R2", Thread.CurrentThread.ManagedThreadId);  
  45.             Monitor.Enter(R2);   //对R2加锁  
  46.             System.Console.WriteLine("线程{0}独占使用资源R2", Thread.CurrentThread.ManagedThreadId);  
  47.             Thread.Sleep(500);  
  48.             System.Console.WriteLine("线程{0}申请使用资源R1", Thread.CurrentThread.ManagedThreadId);  
  49.             Monitor.Enter(R1);   //对R1加锁  
  50.             System.Console.WriteLine("线程{0}独占使用资源R1", Thread.CurrentThread.ManagedThreadId);  
  51.             Thread.Sleep(500);  
  52.             System.Console.WriteLine("线程{0}资源R1使用完毕,放弃", Thread.CurrentThread.ManagedThreadId);  
  53.             Monitor.Exit(R1);  //对R1解锁  
  54.             System.Console.WriteLine("线程{0}资源R2使用完毕,放弃", Thread.CurrentThread.ManagedThreadId);  
  55.             Monitor.Exit(R2);   //对R2解锁  
  56.         }  
  57.     }  
  58.   
  59.     class SharedResource  
  60.     {  
  61.     }  
  62. }  

        在该例中,线程th1执行时先申请使用R1,然后再申请使用R2,而线程th2执行时先申请R2,然后再申请R1,这样对于线程th1和th2,就会造成各自拥有一个对方需要的资源部释放,而又同时申请一个对方已经占有的资源,必然会造成死锁。


多线程数据存取错误


        当多个线程访问同一个数据时,如果不对读和写的顺序作出限定,例如一个线程正在读而另一个数据尝试写,则读数据的线程得到的数据就可能出错。这也是多线程带来的问题。如例:
      
[csharp]  view plain copy print ?
  1. using System;  
  2. using System.Collections.Generic;  
  3. using System.Text;  
  4. using System.Threading;  
  5.   
  6. namespace SharedResourceLeadToDataError  
  7. {  
  8.     class Program  
  9.     {  
  10.         static void Main(string[] args)  
  11.         {  
  12.             Thread[] ths = new Thread[4];  
  13.             for (int i = 0; i < 4; i++)  
  14.             {  
  15.                 ths[i]=new Thread(increaseCount);  
  16.                 ths[i].Start();  
  17.             }  
  18.             System.Console.ReadKey();  
  19.         }  
  20.   
  21.         static void increaseCount()  
  22.         {  
  23.             Random ran = new Random();  
  24.             Thread.Sleep(ran.Next(100, 5000));  
  25.             int beginNum = SharedResource.Count;  
  26.             System.Console.WriteLine("线程 {0} 读到的起始值为 {1}  ", Thread.CurrentThread.ManagedThreadId, beginNum );  
  27.             for (int i = 0; i < 10000; i++)  
  28.             {  
  29.                beginNum ++;  
  30.             }  
  31.             SharedResource.Count = beginNum;  
  32.             System.Console.WriteLine("线程 {0} 结束,SharedResource.Count={1}", Thread.CurrentThread.ManagedThreadId,SharedResource.Count);  
  33.         }  
  34.     }  
  35.   
  36.     class SharedResource  
  37.     {  
  38.         public static int Count = 0;  
  39.     }  
  40. }  

        四个线程同时读写共享变量ShareResource.Count,由于未对读写进行控制,所以必然会造成数据存取错误!

线程同步与并发访问控制手段


        正如为了解决车辆交通问题,人们建立了红绿灯的交通控制手段一样,可以为线程设定一套控制机制,以实现线程间的同步,以及保证以正确的顺序来访问共享资源。为了保护应用程序的资源不被破坏,为多线程程序提供了三种加锁的机制,分别是:Monitor类、Lock关键字和Mutex类。

1、Monitor类


    (1)使用方法


  •  Monitor对象的Enter方法可用于向共享资源申请一把“独占锁”。当一个线程拥有特定共享资源的独占锁时,尝试访问同一共享资源的其他线程只能等待。
  •  Monitor对象的Exit方法用于释放锁。
  •   要注意:Enter与Exit方法必须严格配对,否则,有可能出现死锁情况。
  •  Monitor可以锁定单个对象,也可以锁定一个类型的静态字段或属性
              1).Monitor.Enter(共享资源对象);
              2).Monitor.Enter(typeof(共享资源类型));
[csharp]  view plain copy print ?
  1. using System;  
  2. using System.Collections.Generic;  
  3. using System.Text;  
  4. using System.Threading;  
  5.   
  6. //展示用Monitor访问共享资源  
  7. namespace UseMonitor1  
  8. {  
  9.     class Program  
  10.     {  
  11.         static void Main(string[] args)  
  12.         {  
  13.             SharedResource obj = new SharedResource();  
  14.   
  15.             Thread[] ths = new Thread[4];  
  16.             for (int i = 0; i < 4; i++)  
  17.             {  
  18.                 ths[i] = new Thread(increaseCount);  
  19.                 ths[i].Start(obj);  
  20.             }  
  21.             System.Console.ReadKey();  
  22.         }  
  23.         static void increaseCount(Object obj)  
  24.         {  
  25.            //访问实例字段  
  26.             VisitDynamicField(obj);  
  27.             //访问静态字段  
  28.             VisitStaticField();  
  29.         }  
  30.   
  31.         //访问静态字段  
  32.         private static void VisitStaticField()  
  33.         {  
  34.             //访问静态字段  
  35.             Monitor.Enter(typeof(SharedResource));  
  36.   
  37.             int beginNumber = SharedResource.StaticCount;  
  38.             System.Console.WriteLine("线程 {0} 读到的StaticCount起始值为 {1}  ", Thread.CurrentThread.ManagedThreadId, beginNumber);  
  39.             for (int i = 0; i < 10000; i++)  
  40.             {  
  41.                 beginNumber++;  
  42.             }  
  43.             SharedResource.StaticCount = beginNumber;  
  44.             System.Console.WriteLine("线程 {0} 结束, SharedResource.StaticCount={1}",  
  45.             Thread.CurrentThread.ManagedThreadId, SharedResource.StaticCount);  
  46.   
  47.             Monitor.Exit(typeof(SharedResource));  
  48.         }  
  49.   
  50.         //访问实例字段  
  51.         private static void VisitDynamicField(Object obj)  
  52.         {  
  53.             Monitor.Enter(obj);  
  54.   
  55.             int beginNumber = (obj as SharedResource).DynamicCount;  
  56.             System.Console.WriteLine("线程 {0} 读到的DynamicCount起始值为 {1}  ", Thread.CurrentThread.ManagedThreadId, beginNumber);  
  57.             for (int i = 0; i < 10000; i++)  
  58.             {  
  59.                 beginNumber++;  
  60.             }  
  61.             (obj as SharedResource).DynamicCount = beginNumber;  
  62.             System.Console.WriteLine("线程 {0} 结束,Obj.DynamicCount={1}",  
  63.             Thread.CurrentThread.ManagedThreadId, (obj as SharedResource).DynamicCount);  
  64.   
  65.             Monitor.Exit(obj);  
  66.         }  
  67.     }  
  68.     //共享资源类  
  69.     class SharedResource  
  70.     {  
  71.         public int DynamicCount = 0;        //多线程共享的实例字段  
  72.         public static int StaticCount = 0;  //多线程共享的静态字段  
  73.     }  
  74. }  
        Monitor类的使用模板:

         Monitor.Enter(共享资源对象); //申请对象锁

        //得到了对象锁,可以对共享资源进行访问,
        //其他线程只能等待
        //访问共享资源
        //对共享资源的访问完成,释放对象锁,
        //让其他线程有机会访问共享资源

         Monitor.Exit(obj);

     (2)Monitor的特殊注意之处:


        Monitor一般只用于访问引用类型的共享资源,如果将其施加于值类型变量,则值类型变量将会被装箱,而当调用Exit方法时,虽然是同一个值类型变量,但实际上此值类型变量又会被第二次装箱,这将导致Enter方法所访问的对象与Exit方法所访问的不是同一个,Monitor对象将会引发SynchronizationLockException。
        因此,不要将Monitor用于值类型!

     (3) Monitor.Wait()和Monitor.Pulse()


        Wait()释放对象上的锁,以便允许其他线程锁定和访问该对象。在其他线程访问对象时,调用线程将等待。
        Pulse(),PulseAll()向一个或多个等待线程发送信号。该信号通知等待线程锁定对象的状态已更改,并且锁的所有者准备释放该锁。等待线程被放置在对象的就绪队列中以便它可以最后接收对象锁。一旦线程拥有了锁,它就可以检查对象的新状态以查看是否达到所需状态。PulseAll与Pulse方法类似,不过它是向所有在阻塞队列中的进程发送通知信号,如果只有一个线程被阻塞,那么请使用Pulse方法。
        注意:Pulse、PulseAll和Wait方法必须从同步的代码块内调用。
        例1:
[csharp]  view plain copy print ?
  1. using System;  
  2. using System.Collections.Generic;  
  3. using System.Text;  
  4. using System.Threading;  
  5.   
  6. namespace UseMonitor2  
  7. {  
  8.     class Program  
  9.     {  
  10.         static void Main(string[] args)  
  11.         {  
  12.             //创建共享资源  
  13.             SharedResource obj = new SharedResource();  
  14.             //创建线程对象并启动  
  15.             Thread tha = new Thread(ThreadMethodA);  
  16.             Thread thb = new Thread(ThreadMethodB);  
  17.             tha.Start(obj);  
  18.             thb.Start(obj);  
  19.   
  20.             //程序暂停  
  21.             System.Console.ReadKey();  
  22.         }  
  23.   
  24.         static void ThreadMethodA(Object obj)  
  25.         {  
  26.             Monitor.Enter(obj);  
  27.             (obj as SharedResource).DynamicCount += 100;  
  28.             System.Console.WriteLine("线程A完成工作,obj.DynamicCount={0}", (obj as SharedResource).DynamicCount);  
  29.             Monitor.Pulse(obj); //通知B线程进入准备队列  
  30.             Monitor.Exit(obj);  
  31.         }  
  32.   
  33.         static void ThreadMethodB(Object obj)  
  34.         {  
  35.              Monitor.Enter(obj);  
  36.             //A线程还未工作,因为字段保持初始值0  
  37.             //如果注释掉此条件判断语句,则有可能会发生死锁  
  38.             if((obj as SharedResource).DynamicCount == 0)  
  39.                 Monitor.Wait(obj);//将本线程阻塞,进入阻塞队列等待  
  40.             (obj as SharedResource).DynamicCount += 100;  
  41.             System.Console.WriteLine("线程B完成工作,obj.DynamicCount={0}", (obj as SharedResource).DynamicCount);  
  42.             Monitor.Exit(obj);  
  43.         }  
  44.     }  
  45.   
  46.     //共享资源类  
  47.     class SharedResource  
  48.     {  
  49.         public int DynamicCount = 0;        //多线程共享的实例字段  
  50.     }  
  51. }  

        例2:
[plain]  view plain copy print ?
  1. using System.Threading;  
  2. public class Program   
  3. {  
  4.    static object ball = new object();  
  5.    public static void Main()   
  6.    {  
  7.       Thread threadPing = new Thread( ThreadPingProc );  
  8.       Thread threadPong = new Thread( ThreadPongProc );  
  9.       threadPing.Start(); threadPong.Start();  
  10.    }  
  11.    static void ThreadPongProc()   
  12.    {  
  13.       System.Console.WriteLine("ThreadPong: Hello!");  
  14.       lock ( ball )  
  15.          for (int i = 0; i < 5; i++)  
  16.          {  
  17.             System.Console.WriteLine("ThreadPong: Pong ");  
  18.             Monitor.Pulse( ball );  
  19.             Monitor.Wait( ball );  
  20.          }  
  21.       System.Console.WriteLine("ThreadPong: Bye!");  
  22.    }  
  23.    static void ThreadPingProc()   
  24.    {  
  25.       System.Console.WriteLine("ThreadPing: Hello!");  
  26.       lock ( ball )  
  27.          for(int i=0; i< 5; i++)  
  28.          {  
  29.             System.Console.WriteLine("ThreadPing: Ping ");  
  30.             Monitor.Pulse( ball );  
  31.             Monitor.Wait( ball );  
  32.          }  
  33.       System.Console.WriteLine("ThreadPing: Bye!");  
  34.    }  
  35. }  
可能的执行结果:
[plain]  view plain copy print ?
  1. ThreadPing: Hello!  
  2. ThreadPing: Ping  
  3. ThreadPong: Hello!  
  4. ThreadPong: Pong  
  5. ThreadPing: Ping  
  6. ThreadPong: Pong  
  7. ThreadPing: Ping  
  8. ThreadPong: Pong  
  9. ThreadPing: Ping  
  10. ThreadPong: Pong  
  11. ThreadPing: Ping  
  12. ThreadPong: Pong  
  13. ThreadPing: Bye!  
        当threadPing进程进入ThreadPingProc锁定ball并调用Monitor.Pulse( ball )后,它通知threadPong从阻塞队列进入准备队列,当threadPing调用Monitor.Wait( ball )阻塞自己后,它放弃了了对ball的锁定,所以threadPong得以执行。        
           因此,可以借助Monitor.Pulse()来控制进程的推进顺序。
 
[plain]  view plain copy print ?
  1. //A线程执行的代码  
  2. lock(obj)  
  3. {  
  4.    //访问共享资源obj  
  5.    Monitor.Pulse(obj); //通知B 线程可以访问共享资源obj了  
  6. }  
  7. ---------------------------------------------------------------  
  8. //B线程执行的代码  
  9. lock(obj)  
  10. {  
  11.    Monitor.Wait(obj); //等待A 线程完成  
  12.    //访问共享资源obj  
  13. }  

2、Lock关键字

        C#使用Lock关键字来简化Monitor的用法。lock就是对Monitor的Enter和Exit的一个封装,而且使用起来更简洁,因此Monitor类的Enter()和Exit()方法的组合使用可以用lock关键字替代。

lock (obj)
{
       //访问共享资源代码段
}
等价于:
Monitor.Enter(obj);
        //访问共享资源代码段
Monitor.Exit(obj);

[csharp]  view plain copy print ?
  1. using System;  
  2. using System.Collections.Generic;  
  3. using System.Text;  
  4. using System.Threading;  
  5.   
  6. //展示用Monitor访问共享资源  
  7. namespace UseMonitor1  
  8. {  
  9.     class Program  
  10.     {  
  11.         static void Main(string[] args)  
  12.         {  
  13.             SharedResource obj = new SharedResource();  
  14.   
  15.             Thread[] ths = new Thread[4];  
  16.             for (int i = 0; i < 4; i++)  
  17.             {  
  18.                 ths[i] = new Thread(increaseCount);  
  19.                 ths[i].Start(obj);  
  20.             }  
  21.             System.Console.ReadKey();  
  22.         }  
  23.         static void increaseCount(Object obj)  
  24.         {  
  25.            //访问实例字段  
  26.             VisitDynamicField(obj);  
  27.             //访问静态字段  
  28.             VisitStaticField();  
  29.         }  
  30.   
  31.         //访问静态字段  
  32.         private static void VisitStaticField()  
  33.         {  
  34.             //访问静态字段  
  35.             lock (typeof(SharedResource))  
  36.             {  
  37.                 int beginNumber = SharedResource.StaticCount;  
  38.                 System.Console.WriteLine("线程 {0} 读到的StaticCount起始值为 {1}  ", Thread.CurrentThread.ManagedThreadId, beginNumber);  
  39.                 for (int i = 0; i < 10000; i++)  
  40.                 {  
  41.                     beginNumber++;  
  42.                 }  
  43.                 SharedResource.StaticCount = beginNumber;  
  44.                 System.Console.WriteLine("线程 {0} 结束, SharedResource.StaticCount={1}",  
  45.                 Thread.CurrentThread.ManagedThreadId, SharedResource.StaticCount);  
  46.             }  
  47.         }  
  48.   
  49.         //访问实例字段  
  50.         private static void VisitDynamicField(Object obj)  
  51.         {  
  52.             lock (obj)  
  53.             {  
  54.                 int beginNumber = (obj as SharedResource).DynamicCount;  
  55.                 System.Console.WriteLine("线程 {0} 读到的DynamicCount起始值为 {1}  ", Thread.CurrentThread.ManagedThreadId, beginNumber);  
  56.                 for (int i = 0; i < 10000; i++)  
  57.                 {  
  58.                     beginNumber++;  
  59.                 }  
  60.                 (obj as SharedResource).DynamicCount = beginNumber;  
  61.                 System.Console.WriteLine("线程 {0} 结束,Obj.DynamicCount={1}",  
  62.                 Thread.CurrentThread.ManagedThreadId, (obj as SharedResource).DynamicCount);  
  63.             }  
  64.         }  
  65.     }  
  66.     //共享资源类  
  67.     class SharedResource  
  68.     {  
  69.         public int DynamicCount = 0;        //多线程共享的实例字段  
  70.         public static int StaticCount = 0;  //多线程共享的静态字段  
  71.     }  
  72. }  

3、自旋锁SpinLock


        当一个线程需要访问共享资源时,它可以调用SpinLock.Enter或SpinLock.TryEnter方法申请独占锁,如果暂时不能获得锁(这时可能运行于另一个CPU核上的线程正在访问共享资源),当前线程就会“空转”若干个时钟周期,然后再次尝试。在这个过程中,线程的状态仍是Running,从而避免了操作系统进行一次线程上下文切换所带来的开销。

[plain]  view plain copy print ?
  1. public class MyType  
  2. {  
  3.     //创建自旋锁对象  
  4.     private SpinLock _spinLock = new SpinLock();  
  5.     //将被多线程执行的代码,  
  6.     //由于使用了自旋锁,可以保证被“锁定”代码一次只会被一个线程执行  
  7.     public void DoWork()  
  8.     {  
  9.         bool lockTaken = false;   
  10.         try  
  11.         {  
  12.             _spinLock.Enter(ref lockTaken); //申请获取“锁”  
  13.             // 获得了锁,在此书写工作代码,这些工作代码不会同时被两个线程执行  
  14.         }  
  15.         finally   
  16.         {   
  17.         //工作完毕,或者发生异常时,检查一下当前线程是否占有了锁  
  18.         //如果占有了锁,释放它,以避免出现死锁的情况。  
  19.             if (lockTaken)  _spinLock.Exit();  
  20.         }  
  21.     }  
  22. }  

4、实现原子操作——Interlocked类


        Interlocked类是一种互锁操作,提供对多个线程共享的变量进行同步访问的方法,互锁操作具有 原子性,即整个操作时不能由相同变量上的另一个互锁操作所中断的单元。
        这个类提供了Increment、Decrement、Add静态方法用于对int或long型变量的递增、递减或相加操作。还提供了Exchange(为整型或引用对象赋值)、CompareExchange(比较后再对整型或引用对象赋值),用于为整型或引用类型的赋值提供原子操作。
        在大多数计算机上,增加变量操作不是一个原子操作,需要执行下列步骤:
  1. 将实例变量中的值加载到寄存器中。
  2. 增加或减少该值。
  3. 在实例变量中存储该值。
        如果不使用 Increment 和 Decrement,线程会在执行完前两个步骤后被抢先。 然后由另一个线程执行所有三个步骤。 当第一个线程重新开始执行时,它覆盖实例变量中的值,造成第二个线程执行增减操作的结果丢失。

利用Interlocked类类解决生产者-消费者关系中的竞争条件问题:(例子来自《周长发——c#面向对象编程》
[csharp]  view plain copy print ?
  1. // Interlocked.cs  
  2. // Interlocked示例  
  3.   
  4. using System;  
  5. using System.Threading;  
  6.   
  7. class Test  
  8. {  
  9.     private long bufferEmpty = 0;  
  10.     private string buffer = null;  
  11.   
  12.     static void Main()  
  13.     {  
  14.         Test t = new Test();  
  15.         // 进行测试  
  16.         t.Go();  
  17.     }  
  18.   
  19.     public void Go()  
  20.     {  
  21.         Thread t1 = new Thread(new ThreadStart(Producer));  
  22.         t1.Name = "生产者线程";  
  23.         t1.Start();  
  24.   
  25.         Thread t2 = new Thread(new ThreadStart(Consumer));  
  26.         t2.Name = "消费者线程";  
  27.         t2.Start();  
  28.   
  29.         // 等待两个线程结束  
  30.         t1.Join();  
  31.         t2.Join();  
  32.     }  
  33.   
  34.     // 生产者方法  
  35.     public void Producer()  
  36.     {  
  37.         Console.WriteLine("{0}:开始执行", Thread.CurrentThread.Name);  
  38.   
  39.         try  
  40.         {  
  41.             for (int j = 0; j < 16; ++j)  
  42.             {  
  43.                 // 等待共享缓冲区为空  
  44.                 while (Interlocked.Read(ref bufferEmpty) != 0)   
  45.                     Thread.Sleep(100);  
  46.   
  47.                 // 构造共享缓冲区  
  48.                 Random r = new Random();  
  49.                 int bufSize = r.Next() % 64;  
  50.                 char[] s = new char[bufSize];  
  51.                 for (int i = 0; i < bufSize; ++i)  
  52.                 {  
  53.                     s[i] = (char)((int)'A' + r.Next() % 26);  
  54.                 }  
  55.                 buffer = new string(s);  
  56.   
  57.                 Console.WriteLine("{0}:{1}", Thread.CurrentThread.Name, buffer);  
  58.   
  59.                 // 互锁加一,成为1,标志共享缓冲区已满  
  60.                 Interlocked.Increment(ref bufferEmpty);  
  61.   
  62.                 // 休眠,将时间片让给消费者  
  63.                 Thread.Sleep(10);  
  64.             }  
  65.   
  66.             Console.WriteLine("{0}:执行完毕", Thread.CurrentThread.Name);  
  67.         }  
  68.         catch (System.Threading.ThreadInterruptedException)  
  69.         {  
  70.             Console.WriteLine("{0}:被终止", Thread.CurrentThread.Name);  
  71.         }  
  72.     }  
  73.   
  74.     // 消费者方法  
  75.     public void Consumer()  
  76.     {  
  77.         Console.WriteLine("{0}:开始执行", Thread.CurrentThread.Name);  
  78.   
  79.         try  
  80.         {  
  81.             for (int j = 0; j < 16; ++j)  
  82.             {  
  83.                 while (Interlocked.Read(ref bufferEmpty) == 0)  
  84.                     Thread.Sleep(100);  
  85.   
  86.                 // 打印共享缓冲区  
  87.                 Console.WriteLine("{0}:{1}", Thread.CurrentThread.Name, buffer);  
  88.   
  89.                 // 互锁减一,成为0,标志共享缓冲区已空  
  90.                 Interlocked.Decrement(ref bufferEmpty);  
  91.   
  92.                 // 休眠,将时间片让给生产者  
  93.                 Thread.Sleep(10);  
  94.             }  
  95.   
  96.             Console.WriteLine("{0}:执行完毕", Thread.CurrentThread.Name);  
  97.         }  
  98.         catch (System.Threading.ThreadInterruptedException)  
  99.         {  
  100.             Console.WriteLine("{0}:被终止", Thread.CurrentThread.Name);  
  101.         }  
  102.     }  
  103. }  

5、Mutex类


          Mutex与Monitor类似,需要注意的是Mutex分两种:一种是本地Mutex一种是系统级Mutex,系统级Mutex可以用来进行跨进程间的线程的同步。尽管 mutex 可以用于进程内的线程同步,但是使用 Monitor 通常更为可取,因为监视器是专门为 .NET Framework 而设计的,因而它可以更好地利用资源。相比之下,Mutex 类是 Win32 构造的包装。尽管 mutex 比监视器更为强大,但是相对于 Monitor 类,它所需要的互操作转换更消耗计算资源。
        一个线程要想访问共享资源,它必须调用Mutex对象的Wait系列方法之一提出申请。当申请得到批准的线程完成了对于共享资源的访问后,它调用Mutex对象的ReleaseMutex()方法释放对于共享资源的访问权。

       利用多线程模拟3个人在ATM上多次提款操作:
[csharp]  view plain copy print ?
  1. using System;  
  2. using System.Collections.Generic;  
  3. using System.Text;  
  4. using System.Threading;  
  5.   
  6. namespace UseATM  
  7. {  
  8.     class Program  
  9.     {  
  10.         static ATM OneATM=new ATM(); //共享资源  
  11.         static void Main(string[] args)  
  12.         {  
  13.             //向公共帐号存款2万  
  14.   
  15.             Console.Write("输入公司公共帐户的金额:");  
  16.             int PublicAcountMoney =Convert.ToInt32(Console.ReadLine());  
  17.             OneATM.Deposit(PublicAcountMoney);  
  18.   
  19.             Console.Write("输入ATM中的现金额:");  
  20.             int ATMLeftMoney = Convert.ToInt32(Console.ReadLine());  
  21.             OneATM.SetATMLeftMoney(ATMLeftMoney);  
  22.   
  23.             System.Console.WriteLine("\n敲任意键从公共帐户中取钱,ESC键退出……\n");  
  24.   
  25.             while (System.Console.ReadKey(true).Key !=ConsoleKey.Escape)  
  26.             {  
  27.                 System.Console.WriteLine("");  
  28.                 Thread One = new Thread(WithDrawMoney);  
  29.                 Thread Two = new Thread(WithDrawMoney);  
  30.                 Thread Three = new Thread(WithDrawMoney);  
  31.   
  32.                 //随机生成一个要提款的数额,最少100元,最高5000元  
  33.                 Random ran = new Random();  
  34.                 One.Start(ran.Next(100, 5000));  
  35.                 Two.Start(ran.Next(100, 5000));  
  36.                 Three.Start(ran.Next(100, 5000));  
  37.   
  38.                 //等三人取完钱  
  39.                 One.Join();  
  40.                 Two.Join();  
  41.                 Three.Join();  
  42.   
  43.                 System.Console.WriteLine("公共账号剩余{0}元,ATM中可提现金:{1}", OneATM.QueryPublicAccount(),OneATM.QueryATMLeftAccount());  
  44.             }  
  45.         }  
  46.   
  47.         //线程函数  
  48.         static void WithDrawMoney(object amount)  
  49.         {  
  50.             switch(OneATM.WithDraw((int)amount))  
  51.             {  
  52.                 case WithDrawState.Succeed:  
  53.                     System.Console.WriteLine("成功取出{0}元。",amount );  
  54.                     break;  
  55.                 case WithDrawState.ATMHasNotEnoughCash:  
  56.                     System.Console.WriteLine("ATM中现金不足,无法支取{0}元。", amount);  
  57.                     break ;  
  58.                 case WithDrawState.AccountHasNotEnoughMoney:  
  59.                     System.Console.WriteLine("帐户中没钱了!无法取出{0}元",amount);  
  60.                     break ;  
  61.             }                
  62.         }  
  63.     }  
  64.   
  65.     //自助取款机  
  66.     class ATM  
  67.     {  
  68.         private int PublicAcountLeftMoney;//帐户剩余的钱  
  69.         private int ATMLeftMoney;//提款机剩余的钱  
  70.   
  71.         //同步信息号量  
  72.         private Mutex m = new Mutex();  
  73.   
  74.         //取钱  
  75.         public WithDrawState WithDraw(int amount)  
  76.         {  
  77.             m.WaitOne();  
  78.             //公共帐号钱不够  
  79.             if (PublicAcountLeftMoney < amount)  
  80.             {  
  81.                 m.ReleaseMutex();  
  82.                 return WithDrawState.AccountHasNotEnoughMoney;  
  83.             }  
  84.             //ATM现金不够  
  85.             if (ATMLeftMoney < amount)  
  86.             {  
  87.                 m.ReleaseMutex();  
  88.                 return WithDrawState.ATMHasNotEnoughCash;  
  89.             }  
  90.             //用户可以提取现金  
  91.             ATMLeftMoney -= amount;  
  92.             PublicAcountLeftMoney -= amount;  
  93.             m.ReleaseMutex();  
  94.             return WithDrawState.Succeed;  
  95.         }  
  96.         //存钱  
  97.         public void Deposit(int amount)  
  98.         {  
  99.             m.WaitOne();  
  100.             PublicAcountLeftMoney += amount;  
  101.             m.ReleaseMutex();  
  102.         }  
  103.   
  104.         /// <summary>  
  105.         /// 设置ATM的现金金额  
  106.         /// </summary>  
  107.         /// <param name="amount"></param>  
  108.         public void SetATMLeftMoney(int amount)  
  109.         {  
  110.             Interlocked.Exchange(ref ATMLeftMoney, amount);  
  111.         }  
  112.         //获取还剩余多少钱  
  113.         public int QueryPublicAccount()  
  114.         {  
  115.             return PublicAcountLeftMoney;  
  116.         }  
  117.   
  118.         /// <summary>  
  119.         /// 查询ATM剩余多少钱  
  120.         /// </summary>  
  121.         /// <returns></returns>  
  122.         public int QueryATMLeftAccount()  
  123.         {  
  124.             return ATMLeftMoney;  
  125.         }  
  126.     }  
  127.     //取款状态  
  128.     public enum WithDrawState  
  129.     {  
  130.         Succeed,        //取钱成功  
  131.         AccountHasNotEnoughMoney, //账号中没钱了  
  132.         ATMHasNotEnoughCash  //ATM中没有足够的现金  
  133.     }  
  134. }  
可能的运行结果:
[csharp]  view plain copy print ?
  1. 输入公司公共帐户的金额:200000  
  2. 输入ATM中的现金额:6000000  
  3.   
  4. 敲任意键从公共帐户中取钱,ESC键退出……  
  5.   
  6.   
  7. 成功取出1249元。  
  8. 成功取出643元。  
  9. 成功取出4958元。  
  10. 公共账号剩余193150元,ATM中可提现金:5993150  
  11.   
  12. 成功取出1168元。  
  13. 成功取出3650元。  
  14. 成功取出2707元。  
  15. 公共账号剩余185625元,ATM中可提现金:5985625  
  16.   
  17. 成功取出3866元。  
  18. 成功取出402元。  
  19. 成功取出2397元。  
  20. 公共账号剩余178960元,ATM中可提现金:5978960  
  21.   
  22. 成功取出4485元。  
  23. 成功取出1701元。  
  24. 成功取出3354元。  
  25. 公共账号剩余169420元,ATM中可提现金:5969420  

        Mustex与Monitor有一个很大的区别:
        Mutex可以用来同步属于不同应用程序或者进程的线程,而Monitor没有这个能力。

        为了说明这个区别,我们将生产者和消费者线程分别放在两个应用程序中,在两个应用程序中都各自创建一个同名的Mutex对象,并利用他们来对生产者和消费者线程同步:
生产者线程所在应用程序代码:
[csharp]  view plain copy print ?
  1. // Mutex1.cs  
  2. // Mutex1示例  
  3.   
  4. using System;  
  5. using System.IO;  
  6. using System.Threading;  
  7. using System.Diagnostics;  
  8.   
  9. class Test  
  10. {  
  11.     static void Main()  
  12.     {  
  13.         Test t = new Test();  
  14.         // 进行测试  
  15.         t.Go();  
  16.     }  
  17.   
  18.     public void Go()  
  19.     {  
  20.         // 创建并启动线程  
  21.         Thread t1 = new Thread(new ThreadStart(Producer));  
  22.         t1.Name = "生产者线程";  
  23.         t1.Start();  
  24.   
  25.         // 等待线程结束  
  26.         t1.Join();  
  27.   
  28.         Console.WriteLine("按Enter键退出...");  
  29.         Console.Read();  
  30.     }  
  31.   
  32.     // 生产者方法  
  33.     public void Producer()  
  34.     {  
  35.         Console.WriteLine("{0}:开始执行", Thread.CurrentThread.Name);  
  36.   
  37.         // 创建互斥体  
  38.         Mutex mutex = new Mutex(false"CSharp_Mutex_test");  
  39.   
  40.         // 启动消费者进程  
  41.         Process.Start("Mutex2.exe");  
  42.   
  43.         for (int j = 0; j < 16; ++j)  
  44.         {  
  45.             try  
  46.             {  
  47.                 // 进入互斥体  
  48.                 mutex.WaitOne();  
  49.   
  50.                 FileStream fs = new FileStream(@"d:\text.txt", FileMode.OpenOrCreate, FileAccess.Write);  
  51.                 StreamWriter sw = new StreamWriter(fs);  
  52.                 // 构造字符串  
  53.                 Random r = new Random();  
  54.                 int bufSize = r.Next() % 64;  
  55.                 char[] s = new char[bufSize];  
  56.                 for (int i = 0; i < bufSize; ++i)  
  57.                 {  
  58.                     s[i] = (char)((int)'A' + r.Next() % 26);  
  59.                 }  
  60.                 string str = new string(s);  
  61.                 // 将字符串写入文件  
  62.                 sw.WriteLine(str);  
  63.                 sw.Close();  
  64.   
  65.                 Console.WriteLine("{0}:{1}", Thread.CurrentThread.Name, str);  
  66.             }  
  67.             catch (System.Threading.ThreadInterruptedException)  
  68.             {  
  69.                 Console.WriteLine("{0}:被终止", Thread.CurrentThread.Name);  
  70.                 break;  
  71.             }  
  72.             finally  
  73.             {  
  74.                 // 退出互斥体  
  75.                 mutex.ReleaseMutex();  
  76.             }  
  77.   
  78.             // 休眠,将时间片让给消费者  
  79.             Thread.Sleep(1000);  
  80.         }  
  81.   
  82.         // 关闭互斥体  
  83.         mutex.Close();  
  84.         Console.WriteLine("{0}:执行完毕", Thread.CurrentThread.Name);  
  85.     }  
  86. }  
消费者线程所在应用程序代码:
[csharp]  view plain copy print ?
  1. // Mutex2.cs  
  2. // Mutex2示例  
  3.   
  4. using System;  
  5. using System.IO;  
  6. using System.Threading;  
  7.   
  8. class Test  
  9. {  
  10.     static void Main()  
  11.     {  
  12.         Test t = new Test();  
  13.         // 进行测试  
  14.         t.Go();  
  15.     }  
  16.   
  17.     public void Go()  
  18.     {  
  19.         // 创建并启动线程  
  20.         Thread t2 = new Thread(new ThreadStart(Consumer));  
  21.         t2.Name = "消费者线程";  
  22.         t2.Start();  
  23.   
  24.         // 等待线程结束  
  25.         t2.Join();  
  26.   
  27.         Console.WriteLine("按Enter键退出...");  
  28.         Console.Read();  
  29.     }  
  30.   
  31.     // 消费者方法  
  32.     public void Consumer()  
  33.     {  
  34.         Console.WriteLine("{0}:开始执行", Thread.CurrentThread.Name);  
  35.   
  36.         // 创建互斥体  
  37.         Mutex mutex = new Mutex(false"CSharp_Mutex_test");  
  38.   
  39.         for (int j = 0; j < 16; ++j)  
  40.         {  
  41.             try  
  42.             {  
  43.                 // 进入互斥体  
  44.                 mutex.WaitOne();  
  45.   
  46.                 StreamReader sr = new StreamReader(@"d:\text.txt");  
  47.                 string s = sr.ReadLine();  
  48.                 sr.Close();  
  49.   
  50.                 // 显示字符串的值  
  51.                 Console.WriteLine("{0}:{1}", Thread.CurrentThread.Name, s);  
  52.             }  
  53.             catch (System.Threading.ThreadInterruptedException)  
  54.             {  
  55.                 Console.WriteLine("{0}:被终止", Thread.CurrentThread.Name);  
  56.                 break;  
  57.             }  
  58.             finally  
  59.             {  
  60.                 // 退出互斥体  
  61.                 mutex.ReleaseMutex();  
  62.             }  
  63.   
  64.             // 休眠,将时间片让给消费者  
  65.             Thread.Sleep(1000);  
  66.         }  
  67.   
  68.         // 关闭互斥体  
  69.         mutex.Close();  
  70.         Console.WriteLine("{0}:执行完毕", Thread.CurrentThread.Name);  
  71.     }  
  72. }  

我们分别编译这两个文件,然后运行Mutex1,他会在另一个窗口中启动Mutex2,可能的结果如下:
多线程同步与并发访问共享资源工具—Lock、Monitor、Mutex、Semaphore_第1张图片


6、Semaphore


        Semaphore可以 限制可同时访问某一资源或资源池的线程数。
        Semaphore类在内部维护一个计数器,当一个线程调用Semaphore对象的Wait系列方法时,此计数器减一,只要计数器还是一个正数,线程就不会阻塞。当计数器减到0时,再调用Semaphore对象Wait系列方法的线程将被阻塞,直到有线程调用Semaphore对象的Release()方法增加计数器值时,才有可能解除阻塞状态。

示例说明:
图书馆都配备有若干台公用计算机供读者查询信息,当某日读者比较多时,必须排队等候。UseLibraryComputer实例用多线程模拟了多人使用多台计算机的过程

[csharp]  view plain copy print ?
  1. using System;  
  2. using System.Collections.Generic;  
  3. using System.Text;  
  4. using System.Threading;  
  5.   
  6. namespace UseLibraryComputer  
  7. {  
  8.     class Program  
  9.     {  
  10.         //图书馆拥有的公用计算机  
  11.         private const int ComputerNum = 3;  
  12.         private static Computer[] LibraryComputers;  
  13.         //同步信号量  
  14.         public static  Semaphore sp = new Semaphore( ComputerNum, ComputerNum);  
  15.   
  16.         static void Main(string[] args)  
  17.         {  
  18.             //图书馆拥有ComputerNum台电脑  
  19.             LibraryComputers = new Computer[ComputerNum];  
  20.             for (int i = 0; i <ComputerNum; i++)  
  21.                 LibraryComputers[i] = new Computer("Computer"+(i+1).ToString());  
  22.             int peopleNum = 0;  
  23.             Random ran=new Random();  
  24.             Thread user;  
  25.             System.Console.WriteLine("敲任意键模拟一批批的人排队使用{0}台计算机,ESC键结束模拟……" ,ComputerNum);  
  26.             //每次创建若干个线程,模拟人排队使用计算机  
  27.             while (System.Console.ReadKey().Key  != ConsoleKey.Escape)  
  28.             {  
  29.                 peopleNum = ran.Next(0, 10);  
  30.                 System.Console.WriteLine("\n有{0}人在等待使用计算机。",peopleNum );  
  31.   
  32.                 for (int i = 1; i <= peopleNum; i++)  
  33.                 {  
  34.                     user = new Thread(UseComputer);  
  35.                     user.Start("User" + i.ToString());  
  36.                 }  
  37.             }  
  38.         }  
  39.   
  40.         //线程函数  
  41.         static void UseComputer(Object UserName)  
  42.         {  
  43.             sp.WaitOne();//等待计算机可用  
  44.               
  45.             //查找可用的计算机  
  46.             Computer cp=null;  
  47.             for (int i = 0; i < ComputerNum; i++)  
  48.                 if (LibraryComputers[i].IsOccupied == false)  
  49.                 {  
  50.                     cp = LibraryComputers[i];  
  51.                     break;  
  52.                 }  
  53.             //使用计算机工作  
  54.             cp.Use(UserName.ToString());  
  55.   
  56.             //不再使用计算机,让出来给其他人使用  
  57.             sp.Release();  
  58.         }  
  59.     }  
  60.   
  61.     class Computer  
  62.     {  
  63.         public readonly string ComputerName = "";  
  64.         public Computer(string Name)  
  65.         {  
  66.             ComputerName = Name;  
  67.         }  
  68.         //是否被占用  
  69.         public  bool IsOccupied = false;  
  70.         //人在使用计算机  
  71.         public  void Use(String userName)  
  72.         {  
  73.             System.Console.WriteLine("{0}开始使用计算机{1}", userName,ComputerName);  
  74.             IsOccupied = true;  
  75.             Thread.Sleep(new Random().Next(1, 2000)); //随机休眠,以模拟人使用计算机  
  76.             System.Console.WriteLine("{0}结束使用计算机{1}", userName,ComputerName);  
  77.             IsOccupied = false;  
  78.         }  
  79.     }  
  80. }  
可能的运行结果:
多线程同步与并发访问共享资源工具—Lock、Monitor、Mutex、Semaphore_第2张图片
本文为转载,http://blog.csdn.net/onejune2013/article/details/7621788 但为了更多的人看见,所以作为原创发布到首页,请见谅!

你可能感兴趣的:(thread,线程)