线程同步小结

对于引用类型和非线程安全的资源的同步处理,有四种相关处理: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;

   其中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);
    }
}

    这里要特别注意的是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!");
   }
}

   执行结果如下(有可能是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 类,它所需要的互操作转换更消耗计算资源。
注:文中代码示例来源于MSDN和CodeProject

 

Mutex提供了与C#的lock语句同样的功能,这使它大多时候变得的冗余了。它的优势在于它可以跨进程工作——提供了一计算机范围的锁而胜于程序范围的锁。

Mutex是相当快的,而lock 又要比它快上数百倍,获取Mutex需要花费几微秒,获取lock需花费数十纳秒(假定没有阻止)。

对于一个Mutex类,WaitOne获取互斥锁,当被抢占后时发生阻止。互斥锁在执行了ReleaseMutex之后被释放,就像C#的lock语句一样,Mutex只能从获取互斥锁的这个线程上被释放。

Mutex在跨进程的普遍用处是确保在同一时刻只有一个程序的的实例在运行,下面演示如何使用:

class OneAtATimePlease {
  // 使用一个应用程序的唯一的名称(比如包括你公司的URL)
  static Mutex mutex = new Mutex (false, "oreilly.com OneAtATimeDemo");
  
  static void Main() {
    //等待5秒如果存在竞争——存在程序在
    // 进程中的的另一个实例关闭之后
 
    if (!mutex.WaitOne (TimeSpan.FromSeconds (5), false)) {
      Console.WriteLine ("Another instance of the app is running. Bye!");
      return;
    }
    try {
      Console.WriteLine ("Running - press Enter to exit");
      Console.ReadLine();
    }
    finally { mutex.ReleaseMutex(); }
  }
}

Mutex有个好的特性是,如果程序结束时而互斥锁没通过ReleaseMutex首先被释放,CLR将自动地释放Mutex。

Semaphore

Semaphore就像一个夜总会:它有固定的容量,这由保镖来保证,一旦它满了就没有任何人可以再进入这个夜总会,并且在其外会形成一个队列。然后,当人一个人离开时,队列头的人便可以进入了。构造器需要至少两个参数——夜总会的活动的空间,和夜总会的容量。

Semaphore 的特性与Mutex 和 lock有点类似,除了Semaphore没有“所有者”——它是不可知线程的,任何在Semaphore内的线程都可以调用Release,而Mutex 和 lock仅有那些获取了资源的线程才可以释放它。

在下面的例子中,10个线程执行一个循环,在中间使用Sleep语句。Semaphore确保每次只有不超过3个线程可以执行Sleep语句:

class SemaphoreTest {
  static Semaphore s = new Semaphore (3, 3);  // Available=3; Capacity=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);   // 每次只有3个线程可以到达这里
      s.Release();
    }
  }
}


C#笔记20:多线程之线程同步中的信号量AutoResetEvent和ManualResetEvent

本章概要:

1:终止状态和非终止状态

2:AutoResetEvent和ManualResetEvent的区别

3:WaitHandle.WaitOne()等

1:终止状态和非终止状态

     首先说说线程的终止状态和非终止状态。AutoResetEvent和ManualResetEvent的构造函数中,都有bool变量来指明线程的终止状态和非终止状态。true表示终止状态,false表示非终止状态。看代码片段1:

代码片段1:

        AutoResetEvent _autoResetEvent = new AutoResetEvent(false);

        private void BT_Temp_Click(object sender, RoutedEventArgs e) 
        { 
            Thread t1 = new Thread(this.Thread1Foo); 
            t1.Start(); 
            Thread.Sleep(3000); 
            _autoResetEvent.Set(); 
        }

        void Thread1Foo() 
        { 
            _autoResetEvent.WaitOne(); 
            MessageBox.Show("t1 end"); 
        } 

这段代码的执行结果,就是3秒钟过后,弹出“t1 end”。 
    而如果把: 
    AutoResetEvent _autoResetEvent = new AutoResetEvent(false); 
    改为: 
    AutoResetEvent _autoResetEvent = new AutoResetEvent(true); 
    则“t1 end”将会立刻弹出。 
    也就是说,在终止状态中,_autoResetEvent.WaitOne()是不会起到阻滞工作线程的作用的。(PS:ManualResetEvent也同样)

二:AutoResetEvent和ManualResetEvent的区别 
    接下来,再来看看AutoResetEvent和ManualResetEvent的区别。我们看代码段2和代码段3:

代码段2: 
     AutoResetEvent _autoResetEvent = new AutoResetEvent(false); 
        private void BT_Temp_Click(object sender, RoutedEventArgs e) 
        { 
            Thread t1 = new Thread(this.Thread1Foo); 
            t1.Start(); 
            Thread t2 = new Thread(this.Thread2Foo); 
            t2.Start(); 
            Thread.Sleep(3000); 
            _autoResetEvent.Set(); 
        }

        void Thread1Foo() 
        { 
            _autoResetEvent.WaitOne(); 
            MessageBox.Show("t1 end"); 
        }

        void Thread2Foo() 
        { 
            _autoResetEvent.WaitOne(); 
            MessageBox.Show("t2 end"); 
        } 


    该段代码运行的效果是,过3秒后,要么弹出“t1 end”,要么弹出“t2 end”,不会两个都弹出。也就是说,其中一个进行将会结束,而另一个进程永远不会结束。

    代码段3:

复制代码
        ManualResetEvent _menuRestEvent = new ManualResetEvent(false);

        private void BT_Temp_Click(object sender, RoutedEventArgs e) 
        { 
            Thread t1 = new Thread(this.Thread1Foo); 
            t1.Start(); 
            Thread t2 = new Thread(this.Thread2Foo); 
            t2.Start(); 
            Thread.Sleep(3000); 
            _menuRestEvent.Set(); 
        }

        void Thread1Foo() 
        { 
            _menuRestEvent.WaitOne(); 
            MessageBox.Show("t1 end"); 
        }

        void Thread2Foo() 
        { 
            _menuRestEvent.WaitOne(); 
            MessageBox.Show("t2 end"); 
        } 
复制代码

    该段代码运行的效果是,过3秒后,“t1 end”和“t2 end”,两个都被弹出。也就是说,两个进程都结束了。 
    这个特性就是说,AutoResetEvent只会给一个线程发送信号,而不会给多个线程发送信号。在我们需要同步多个线程的时候,就只能采用ManualResetEvent了。至于深层次的原因是,AutoResetEvent在set()之后,会将线程状态自动置为false,而ManualResetEvent在Set()后,线程的状态就变为true了,必须手动ReSet()之后,才会重新将线程置为false。这也就是为什么他们的名字一个为Auto,一个为Manual的原因。为了更加充分的验证ManualResetEvent的这点特性,我们再来看代码片段4

    代码片段4: 

        ManualResetEvent _menuRestEvent = new ManualResetEvent(false);

        private void BT_Temp_Click(object sender, RoutedEventArgs e) 
        { 
            Thread t1 = new Thread(this.Thread1Foo); 
            t1.Start(); 
            Thread t2 = new Thread(this.Thread2Foo); 
            t2.Start(); 
            Thread.Sleep(3000); 
            _menuRestEvent.Set(); 
            //_menuRestEvent.Reset();            
        }

        void Thread1Foo() 
        { 
            _menuRestEvent.WaitOne(); 
            MessageBox.Show("t1 step1 end"); 
            //睡1S,用于等待主线程_menuRestEvent.Reset(); 
            Thread.Sleep(1000); 
            _menuRestEvent.WaitOne(); 
            MessageBox.Show("t1 step2 end"); 
        }

        void Thread2Foo() 
        { 
            _menuRestEvent.WaitOne(); 
            MessageBox.Show("t2 step1 end"); 
            //睡1S,用于等待主线程_menuRestEvent.Reset(); 
            Thread.Sleep(1000); 
            _menuRestEvent.WaitOne(); 
            MessageBox.Show("t2 step2 end"); 
        } 

    在代码片段4中,我们对//_menuRestEvent.Reset()进行了注释,也就是说, _menuRestEvent.Set()后,线程的状态就是true状态的,程序运行的结果是"t1 step1 end"、"t1 step2 end"、"t1 step2 end"、"t2 step2 end"在3秒之后全部弹出。 
    而如果我们将//_menuRestEvent.Reset()的注释去掉,会发现"t1 step2 end"和"t2 step2 end"永远不会弹出。除非我们在主线程中再次对_menuRestEvent进行Set()。

3:WaitHandle.WaitOne()等

    无论是用什么方法启动一个新线程,我们都可以得到这个线程的WaitHandle,而使用WaitOne等方法的作用就是组织当前线程,直到有其它线程给当前线程发送信号。

    我们来看看一个很能说明该问题的例子,该例子的描述如下:

    模拟通信中的客户端。客户端在运行过程中,服务器每隔一段的时间会给客户端发送心跳数据。客户端启动一个线程,每3秒检测是否收到心跳数据,如果没有心跳数据,则告诉自己已经和服务器断开连接。代码如下:

        Thread t;
        AutoResetEvent _autoResetEvent = new AutoResetEvent(false);
            
        private void button1_Click(object sender, EventArgs e)
        {
            t = new Thread(new ThreadStart(delegate
                {
                    while (true)
                    {
                        //等3秒,3秒没有信号,显示断开
                        //有信号,则显示更新
                        bool re = _autoResetEvent.WaitOne(3000);
                        ShowRe(re);
                    }
                }));
            t.Start();
            t.IsBackground = true;
        }

        void ShowRe(bool re)
        {
            label1.BeginInvoke(new MethodInvoker(delegate
            {
                if (re)
                {
                    label1.Text = DateTime.Now.ToString() + "保持连接状态";
                }
                else
                {
                    label1.Text = DateTime.Now.ToString() + "断开,需要重启";
                }
            }));
        }

        //模拟服务器发送心跳数据。
        private void button2_Click(object sender, EventArgs e)
        {
            _autoResetEvent.Set();
        }







posted on 2012-08-20 12:10  .net — 粉丝  阅读(138)  评论(0编辑  收藏  举报

导航