在《多线程编程》系列第一篇讲述了如何启动线程,这篇讲述线程之间存在竞争时如何确保同步并且不发生死锁。
线程不同步引出的问题
下面做一个假设,假设有100张票,由两个线程来实现一个售票程序,每次线程运行时首先检查是否还有票未售出,如果有就按照票号从小到大的顺序售出票号最小的票,程序的代码如下:
using System; using System.Collections.Generic; using System.Text; using System.Threading; namespace StartThread { public class ThreadLock { private Thread threadOne; private Thread threadTwo; private List<string> ticketList; private object objLock = new object(); public ThreadLock() { threadOne = new Thread(new ThreadStart(Run)); threadOne.Name = "Thread_1"; threadTwo = new Thread(new ThreadStart(Run)); threadTwo.Name = "Thread_2"; } public void Start() { ticketList = new List<string>(100); for (int i = 1; i <= 100; i++) { ticketList.Add(i.ToString().PadLeft(3,'0'));//实现3位的票号,如果不足3位数,则以0补足3位 } threadOne.Start(); threadTwo.Start(); } private void Run() { while (ticketList.Count > 0)//① { string ticketNo = ticketList[0];//② Console.WriteLine("{0}:售出一张票,票号:{1}", Thread.CurrentThread.Name, ticketNo); ticketList.RemoveAt(0);//③ Thread.Sleep(1); } } } }
在C#中实现线程的同步有几种方法:lock、Mutex、Monitor、Semaphore、Interlocked和ReaderWriterLock等。同步策略也可以分为同步上下文、同步代码区、手动同步几种方式。
同步上下文
同步上下文的策略主要是依靠SynchronizationAttribute类来实现。例如下面的代码就是一个实现了上下文同步的类的代码:
using System; using System.Collections.Generic; using System.Text; //需要添加对System.EnterpriseServices.dll这个类库的引用采用使用这个dll using System.EnterpriseServices; namespace StartThread { [Synchronization(SynchronizationOption.Required)]//确保创建的对象已经同步 public class SynchronizationAttributeClass { public void Run() { } } }
private void Run() { while (ticketList.Count > 0)//① { lock (objLock) { if (ticketList.Count > 0) { string ticketNo = ticketList[0];//② Console.WriteLine("{0}:售出一张票,票号:{1}", Thread.CurrentThread.Name, ticketNo); ticketList.RemoveAt(0);//③ Thread.Sleep(1); } } } }
经过这样处理之后系统的运行结果就会正常。效果如下:
总的来说,lock语句是一种有效的、不跨越多个方法的小代码块同步的做法,也就是使用lock语句只能在某个方法的部分代码之间,不能跨越方法。
Monitor类
针对上面的代码,如果使用Monitor类来同步的话,代码则是如下效果:
private void Run() { while (ticketList.Count > 0)//① { Monitor.Enter(objLock); if (ticketList.Count > 0) { string ticketNo = ticketList[0];//② Console.WriteLine("{0}:售出一张票,票号:{1}", Thread.CurrentThread.Name, ticketNo); ticketList.RemoveAt(0);//③ Thread.Sleep(1); } Monitor.Exit(objLock); } }
当然这段代码最终运行的效果也和使用lock关键字来同步的效果一样。比较之下,大家会发现使用lock关键字来保持同步的差别不大:”lock (objLock){“被换成了”Monitor.Enter(objLock);”,”}”被换成了” Monitor.Exit(objLock);”。实际上如果你通过其它方式查看最终生成的IL代码,你会发现使用lock关键字的代码实际上是用Monitor来实现的。
如下代码:
lock (objLock){ //同步代码 }
实际上是相当于:
try{ Monitor.Enter(objLock); //同步代码 } finally { Monitor.Exit(objLock); }
我们知道在绝大多数情况下finally中的代码块一定会被执行,这样确保了即使同步代码出现了异常也仍能释放同步锁。
Monitor类出了Enter()和Exit()方法之外,还有Wait()和Pulse()方法。Wait()方法是临时释放当前活得的锁,并使当前对象处于阻塞状态,Pulse()方法是通知处于等待状态的对象可以准备就绪了,它一会就会释放锁。下面我们利用这两个方法来完成一个协同的线程,一个线程负责随机产生数据,一个线程负责将生成的数据显示出来。下面是代码:
using System; using System.Collections.Generic; using System.Text; using System.Threading; namespace StartThread { public class ThreadWaitAndPluse { private object lockObject; private int number; private Random random; public ThreadWaitAndPluse() { lockObject = new object(); random = new Random(); } //显示生成数据的线程要执行的方法 public void ThreadMethodOne() { Monitor.Enter(lockObject);//获取对象锁 Console.WriteLine("当前进入的线程:" + Thread.CurrentThread.GetHashCode()); for (int i = 0; i < 5; i++) { Monitor.Wait(lockObject);//释放对象锁,并阻止当前线程 Console.WriteLine("WaitAndPluse1:工作"); Console.WriteLine("WaitAndPluse1:得到了数据,number=" + number + ",Thread ID=" + Thread.CurrentThread.GetHashCode()); //通知其它等待锁的对象状态已经发生改变,当这个对象释放锁之后等待锁的对象将会活得锁 Monitor.Pulse(lockObject); } Console.WriteLine("退出当前线程:" + Thread.CurrentThread.GetHashCode()); Monitor.Exit(lockObject);//释放对象锁 } //生成随机数据线程要执行的方法 public void ThreadMethodTwo() { Monitor.Enter(lockObject);//获取对象锁 Console.WriteLine("当前进入的线程:" + Thread.CurrentThread.GetHashCode()); for (int i = 0; i < 5; i++) { //通知其它等待锁的对象状态已经发生改变,当这个对象释放锁之后等待锁的对象将会活得锁 Monitor.Pulse(lockObject); Console.WriteLine("WaitAndPluse2:工作"); number =random.Next(DateTime.Now.Millisecond);//生成随机数 Console.WriteLine("WaitAndPluse2:生成了数据,number=" + number + ",Thread ID=" + Thread.CurrentThread.GetHashCode()); Monitor.Wait(lockObject);//释放对象锁,并阻止当前线程 } Console.WriteLine("退出当前线程:" + Thread.CurrentThread.GetHashCode()); Monitor.Exit(lockObject);//释放对象锁 } public static void Main() { ThreadWaitAndPluse demo=new ThreadWaitAndPluse(); Thread t1 = new Thread(new ThreadStart(demo.ThreadMethodOne)); t1.Start(); Thread t2 = new Thread(new ThreadStart(demo.ThreadMethodTwo)); t2.Start(); Console.ReadLine(); } } }
执行上面的代码在大部分情况下会看到如下所示的结果:
一般情况下会看到上面的结果,原因是t1的Start()方法在先,所以一般会优先活得执行,t1执行后首先获得对象锁,然后在循环中通过 Monitor.Wait(lockObject)方法临时释放对象锁,t1这时处于阻塞状态;这样t2获得对象锁并且得以执行,t2进入循环后通过Monitor.Pulse(lockObject)方法通知等待同一个对象锁的t1准备好,然后在生成随机数之后临时释放对象锁;接着t1获得了对象锁,执行输出t2生成的数据,之后t1通过 Monitor.Wait(lockObject)通知t2准备就绪,并在下一个循环中通过 Monitor.Wait(lockObject)方法临时释放对象锁,就这样t1和t2交替执行,得到了上面的结果。
当然在某些情况下,可能还会看到如下的结果:
至于为什么会产生这个结果,原因其实很简单,尽管t1.Start()出现在t2.Start()之前,但是并不能就认为t1一定会比t2优先执行(尽管可能在大多数情况下是),还要考虑线程调度问题,使用了多线程之后就会使代码的执行顺序变得复杂起来。在某种情况下t1和t2对锁的使用产生了冲突,形成了死锁,也就出现了如上图所示的情况,为了避免这种情况可以通过让t2延时一个合适的时间。
手控同步
手控同步是指使用不同的同步类来创建自己的同步机制。使用这种策略要求手动地为不同的域或者方法同步。
ReaderWriterLock
ReaderWriterLock支持单个写线程和多个读线程的锁。在任一特定时刻允许多个线程同时进行读操作或者一个线程进行写操作,使用ReaderWriterLock来进行读写同步比使用监视的方式(如Monitor)效率要高。
下面是一个例子,在例子中使用了两个读线程和一个写线程,代码如下:
using System; using System.Collections.Generic; using System.Text; using System.Threading; namespace StartThread { public class ThreadWaitAndPluse { private object lockObject; private int number; private Random random; public ThreadWaitAndPluse() { lockObject = new object(); random = new Random(); } //显示生成数据的线程要执行的方法 public void ThreadMethodOne() { Monitor.Enter(lockObject);//获取对象锁 Console.WriteLine("当前进入的线程:" + Thread.CurrentThread.GetHashCode()); for (int i = 0; i < 5; i++) { Monitor.Wait(lockObject);//释放对象锁,并阻止当前线程 Console.WriteLine("WaitAndPluse1:工作"); Console.WriteLine("WaitAndPluse1:得到了数据,number=" + number + ",Thread ID=" + Thread.CurrentThread.GetHashCode()); //通知其它等待锁的对象状态已经发生改变,当这个对象释放锁之后等待锁的对象将会活得锁 Monitor.Pulse(lockObject); } Console.WriteLine("退出当前线程:" + Thread.CurrentThread.GetHashCode()); Monitor.Exit(lockObject);//释放对象锁 } //生成随机数据线程要执行的方法 public void ThreadMethodTwo() { Monitor.Enter(lockObject);//获取对象锁 Console.WriteLine("当前进入的线程:" + Thread.CurrentThread.GetHashCode()); for (int i = 0; i < 5; i++) { //通知其它等待锁的对象状态已经发生改变,当这个对象释放锁之后等待锁的对象将会活得锁 Monitor.Pulse(lockObject); Console.WriteLine("WaitAndPluse2:工作"); number =random.Next(DateTime.Now.Millisecond);//生成随机数 Console.WriteLine("WaitAndPluse2:生成了数据,number=" + number + ",Thread ID=" + Thread.CurrentThread.GetHashCode()); Monitor.Wait(lockObject);//释放对象锁,并阻止当前线程 } Console.WriteLine("退出当前线程:" + Thread.CurrentThread.GetHashCode()); Monitor.Exit(lockObject);//释放对象锁 } public static void Main() { ThreadWaitAndPluse demo=new ThreadWaitAndPluse(); Thread t1 = new Thread(new ThreadStart(demo.ThreadMethodOne)); t1.Start(); Thread t2 = new Thread(new ThreadStart(demo.ThreadMethodTwo)); t2.Start(); Console.ReadLine(); } } }
using System; using System.Collections.Generic; using System.Text; using System.Threading; namespace StartThread { //下面的代码摘自MSDN,笔者做了中文代码注释 //周公 public class EventWaitHandleDemo { double baseNumber, firstTerm, secondTerm, thirdTerm; AutoResetEvent[] autoEvents; ManualResetEvent manualEvent; //产生随机数的类. Random random; static void Main() { EventWaitHandleDemo ewhd = new EventWaitHandleDemo(); Console.WriteLine("Result = {0}.", ewhd.Result(234).ToString()); Console.WriteLine("Result = {0}.", ewhd.Result(55).ToString()); Console.ReadLine(); } //构造函数 public EventWaitHandleDemo() { autoEvents = new AutoResetEvent[] { new AutoResetEvent(false), new AutoResetEvent(false), new AutoResetEvent(false) }; manualEvent = new ManualResetEvent(false); } //计算基数 void CalculateBase(object stateInfo) { baseNumber = random.NextDouble(); //指示基数已经算好. manualEvent.Set(); } //计算第一项 void CalculateFirstTerm(object stateInfo) { //生成随机数 double preCalc = random.NextDouble(); //等待基数以便计算. manualEvent.WaitOne(); //通过preCalc和baseNumber计算第一项. firstTerm = preCalc * baseNumber *random.NextDouble(); //发出信号指示计算完成. autoEvents[0].Set(); } //计算第二项 void CalculateSecondTerm(object stateInfo) { double preCalc = random.NextDouble(); manualEvent.WaitOne(); secondTerm = preCalc * baseNumber *random.NextDouble(); autoEvents[1].Set(); } //计算第三项 void CalculateThirdTerm(object stateInfo) { double preCalc = random.NextDouble(); manualEvent.WaitOne(); thirdTerm = preCalc * baseNumber *random.NextDouble(); autoEvents[2].Set(); } //计算结果 public double Result(int seed) { random = new Random(seed); //同时计算 ThreadPool.QueueUserWorkItem(new WaitCallback(CalculateFirstTerm)); ThreadPool.QueueUserWorkItem(new WaitCallback(CalculateSecondTerm)); ThreadPool.QueueUserWorkItem(new WaitCallback(CalculateThirdTerm)); ThreadPool.QueueUserWorkItem(new WaitCallback(CalculateBase)); //等待所有的信号. WaitHandle.WaitAll(autoEvents); //重置信号,以便等待下一次计算. manualEvent.Reset(); //返回计算结果 return firstTerm + secondTerm + thirdTerm; } } }
程序的运行结果如下:
Result = 0.355650523270459.
Result = 0.125205692112756.
当然因为引入了随机数,所以每次计算结果并不相同,这里要讲述的是它们之间的控制。首先在 Result(int seed)方法中讲计算基数、第一项、第二项及第三项的方法放到线程池中,要计算第一二三项时首先要确定基数,这些方法通过manualEvent.WaitOne()暂时停止执行,于是计算基数的方法首先执行,计算出基数之后通过manualEvent.Set()方法通知计算第一二三项的方法开始,在这些方法完成计算之后通过autoEvents数组中的AutoResetEvent元素的Set()方法发出信号,标识执行完毕。这样WaitHandle.WaitAll(autoEvents)这一步可以继续执行,从而得到执行结果。
在上面代码中的WaitHandle的其它子类限于篇幅不在这里一一举例讲解,它们在使用了多少有些相似之处(毕竟是一个爹、从一个抽象类继承下来的嘛)。
图片上传功能暂时关闭,敬请谅解。