多线程编程(2):线程的同步

简介:
多线程编程(2):线程的同步
在《多线程编程》系列第一篇讲述了如何启动线程,这篇讲述线程之间存在竞争时如何确保同步并且不发生死锁。
线程不同步引出的问题
下面做一个假设,假设有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); 
                        } 
                } 
        } 


这段程序的执行效果并不每次都一样,下图是某次运行效果的截图:
 
从上图可以看出票号为001的号被售出了两次(如果遇上像《无极》中谢霆锋饰演的那种角色,可能又会引出一场《一张票引发的血案》了,呵呵),为什么会出现这种情况呢?
请看代码③处:
ticketList.RemoveAt(0);//③
在某个情况有可能线程1恰好运行到此处,从ticketList中取出索引为0的那个元素并将票号输出,不巧的是正好分给线程1执行的时间片已用完,线程1进入休眠状态,线程2从头开始执行,它可以从容地从ticketList中取出索引为0的那个元素并且将其输出,因为线程1执行的时候虽然输出了ticketList中索引为0的那个元素但是来不及将其删除,所以这时候线程2得到的值和上次线程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() 
                { 
                } 
        } 

所有在同一个上下文域的对象共享同一个锁。这样创建的对象实例属性、方法和字段就具有线程安全性,需要注意的是类的静态字段、属性和方法是不具有线程安全性的。
同步代码区
同步代码区是另外一种策略,它是针对特定部分代码进行同步的一种方法。
lock同步
针对上面的代码,要实现不会出现混乱(两次卖出同一张票或者有些票根本就没有卖出),可以lock关键字来实现,出现问题的部分就是在于判断剩余票数是否大于0,如果大于0则从当前总票数中减去最大的一张票,因此可以对这部分进行处理,代码如下:
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 ReadWriteLockDemo 
        { 
                 private  int number; 
                 private ReaderWriterLock rwl; 
                 private Random random; 
                 public ReadWriteLockDemo() 
                { 
                        rwl =  new ReaderWriterLock(); 
                        random =  new Random(); 
                } 
                 /// <summary> 
                 /// 读线程要执行的方法 
                 /// </summary> 
                 public  void Read() 
                { 
                        Thread.Sleep(10); //暂停,确保写线程优先执行 
                         for ( int i = 0; i < 5; i++) 
                        { 
                                rwl.AcquireReaderLock(Timeout.Infinite); 
                                Console.WriteLine( "Thread" + Thread.CurrentThread.GetHashCode() +  "读出数据,number="+ number); 
                                Thread.Sleep(500); 
                                rwl.ReleaseReaderLock(); 
                        } 
                } 
                 /// <summary> 
                 /// 写线程要执行的方法 
                 /// </summary> 
                 public  void Write() 
                { 
                         for ( int i = 0; i < 5; i++) 
                        { 
                                rwl.AcquireWriterLock(Timeout.Infinite); 
                                number = random.Next(DateTime.Now.Millisecond); 
                                Thread.Sleep(100); 
                                Console.WriteLine( "Thread" + Thread.CurrentThread.GetHashCode() +  "写入数据,number="+ number); 
                                rwl.ReleaseWriterLock(); 
                        } 
                } 
                 public  static  void Main() 
                { 
                        ReadWriteLockDemo rwld= new ReadWriteLockDemo(); 
                        Thread reader1 =  new Thread( new ThreadStart(rwld.Read)); 
                        Thread reader2 =  new Thread( new ThreadStart(rwld.Read)); 
                        reader1.Start(); 
                        reader2.Start(); 
                        Thread writer1 =  new Thread( new ThreadStart(rwld.Write)); 
                        writer1.Start(); 
                        Console.ReadLine(); 
                } 
        } 

程序的执行效果如下:
 
WaitHandle
WaitHandle类是一个抽线类,有多个类直接或者间接继承自WaitHandle类,类图如下:
 
在WaitHandle类中SignalAndWait、WaitAll、WaitAny及WaitOne这几个方法都有重载形式,其中除WaitOne之外都是静态的。WaitHandle方法常用作同步对象的基类。WaitHandle对象通知其他的线程它需要对资源排他性的访问,其他的线程必须等待,直到WaitHandle不再使用资源和等待句柄没有被使用。
WaitHandle方法有多个Wait的方法,这些方法的区别如下:
WaitAll:等待指定数组中的所有元素收到信号。
WaitAny:等待指定数组中的任一元素收到信号。
WaitOne:当在派生类中重写时,阻塞当前线程,直到当前的 WaitHandle 收到信号。
这些wait方法阻塞线程直到一个或者更多的同步对象收到信号。
下面的是一个MSDN中的例子,讲的是一个计算过程,最终的计算结果为第一项+第二项+第三项,在计算第一、二、三项时需要使用基数来进行计算。在代码中使用了线程池也就是ThreadPool来操作,这里面涉及到计算的顺序的先后问题,通过WaitHandle及其子类可以很好地解决这个问题。
代码如下:
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的其它子类限于篇幅不在这里一一举例讲解,它们在使用了多少有些相似之处(毕竟是一个爹、从一个抽象类继承下来的嘛)。

















本文转自周金桥51CTO博客,原文链接: http://blog.51cto.com/zhoufoxcn/262608 ,如需转载请自行联系原作者


相关文章
|
2月前
|
Java
如何在Java中进行多线程编程
Java多线程编程常用方式包括:继承Thread类、实现Runnable接口、Callable接口(可返回结果)及使用线程池。推荐线程池以提升性能,避免频繁创建线程。结合同步与通信机制,可有效管理并发任务。
160 6
|
5月前
|
Java API 微服务
为什么虚拟线程将改变Java并发编程?
为什么虚拟线程将改变Java并发编程?
306 83
|
5月前
|
安全 算法 Java
Java 多线程:线程安全与同步控制的深度解析
本文介绍了 Java 多线程开发的关键技术,涵盖线程的创建与启动、线程安全问题及其解决方案,包括 synchronized 关键字、原子类和线程间通信机制。通过示例代码讲解了多线程编程中的常见问题与优化方法,帮助开发者提升程序性能与稳定性。
222 0
|
2月前
|
Java 调度 数据库
Python threading模块:多线程编程的实战指南
本文深入讲解Python多线程编程,涵盖threading模块的核心用法:线程创建、生命周期、同步机制(锁、信号量、条件变量)、线程通信(队列)、守护线程与线程池应用。结合实战案例,如多线程下载器,帮助开发者提升程序并发性能,适用于I/O密集型任务处理。
288 0
|
3月前
|
算法 Java
Java多线程编程:实现线程间数据共享机制
以上就是Java中几种主要处理多线程序列化资源以及协调各自独立运行但需相互配合以完成任务threads 的技术手段与策略。正确应用上述技术将大大增强你程序稳定性与效率同时也降低bug出现率因此深刻理解每项技术背后理论至关重要.
261 16
|
7月前
|
机器学习/深度学习 消息中间件 存储
【高薪程序员必看】万字长文拆解Java并发编程!(9-2):并发工具-线程池
🌟 ​大家好,我是摘星!​ 🌟今天为大家带来的是并发编程中的强力并发工具-线程池,废话不多说让我们直接开始。
268 0
|
5月前
|
数据采集 监控 调度
干货分享“用 多线程 爬取数据”:单线程 + 协程的效率反超 3 倍,这才是 Python 异步的正确打开方式
在 Python 爬虫中,多线程因 GIL 和切换开销效率低下,而协程通过用户态调度实现高并发,大幅提升爬取效率。本文详解协程原理、实战对比多线程性能,并提供最佳实践,助你掌握异步爬虫核心技术。
|
6月前
|
Java 数据挖掘 调度
Java 多线程创建零基础入门新手指南:从零开始全面学习多线程创建方法
本文从零基础角度出发,深入浅出地讲解Java多线程的创建方式。内容涵盖继承`Thread`类、实现`Runnable`接口、使用`Callable`和`Future`接口以及线程池的创建与管理等核心知识点。通过代码示例与应用场景分析,帮助读者理解每种方式的特点及适用场景,理论结合实践,轻松掌握Java多线程编程 essentials。
401 5
|
10月前
|
Linux
Linux编程: 在业务线程中注册和处理Linux信号
通过本文,您可以了解如何在业务线程中注册和处理Linux信号。正确处理信号可以提高程序的健壮性和稳定性。希望这些内容能帮助您更好地理解和应用Linux信号处理机制。
198 26
|
10月前
|
Linux
Linux编程: 在业务线程中注册和处理Linux信号
本文详细介绍了如何在Linux中通过在业务线程中注册和处理信号。我们讨论了信号的基本概念,并通过完整的代码示例展示了在业务线程中注册和处理信号的方法。通过正确地使用信号处理机制,可以提高程序的健壮性和响应能力。希望本文能帮助您更好地理解和应用Linux信号处理,提高开发效率和代码质量。
204 17

热门文章

最新文章