`
caobihole
  • 浏览: 946011 次
文章分类
社区版块
存档分类
最新评论

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

 
阅读更多

在《多线程编程》系列第一篇讲述了如何启动线程,这篇讲述线程之间存在竞争时如何确保同步并且不发生死锁。
线程不同步引出的问题
下面做一个假设,假设有100张票,由两个线程来实现一个售票程序,每次线程运行时首先检查是否还有票未售出,如果有就按照票号从小到大的顺序售出票号最小的票,程序的代码如下:

这段程序的执行效果并不每次都一样,下图是某次运行效果的截图:

从上图可以看出票号为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类来实现。例如下面的代码就是一个实现了上下文同步的类的代码:


所有在同一个上下文域的对象共享同一个锁。这样创建的对象实例属性、方法和字段就具有线程安全性,需要注意的是类的静态字段、属性和方法是不具有线程安全性的。
同步代码区
同步代码区是另外一种策略,它是针对特定部分代码进行同步的一种方法。
lock同步
针对上面的代码,要实现不会出现混乱(两次卖出同一张票或者有些票根本就没有卖出),可以lock关键字来实现,出现问题的部分就是在于判断剩余票数是否大于0,如果大于0则从当前总票数中减去最大的一张票,因此可以对这部分进行处理,代码如下:

经过这样处理之后系统的运行结果就会正常。效果如下:

总的来说,lock语句是一种有效的、不跨越多个方法的小代码块同步的做法,也就是使用lock语句只能在某个方法的部分代码之间,不能跨越方法。
Monitor类
针对上面的代码,如果使用Monitor类来同步的话,代码则是如下效果:

当然这段代码最终运行的效果也和使用lock关键字来同步的效果一样。比较之下,大家会发现使用lock关键字来保持同步的差别不大:”lock (objLock){“被换成了”Monitor.Enter(objLock);”,”}”被换成了” Monitor.Exit(objLock);”。实际上如果你通过其它方式查看最终生成的IL代码,你会发现使用lock关键字的代码实际上是用Monitor来实现的。
如下代码:

实际上是相当于:

我们知道在绝大多数情况下finally中的代码块一定会被执行,这样确保了即使同步代码出现了异常也仍能释放同步锁。
Monitor类出了Enter()和Exit()方法之外,还有Wait()和Pulse()方法。Wait()方法是临时释放当前活得的锁,并使当前对象处于阻塞状态,Pulse()方法是通知处于等待状态的对象可以准备就绪了,它一会就会释放锁。下面我们利用这两个方法来完成一个协同的线程,一个线程负责随机产生数据,一个线程负责将生成的数据显示出来。下面是代码:

执行上面的代码在大部分情况下会看到如下所示的结果:

一般情况下会看到上面的结果,原因是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)效率要高。
下面是一个例子,在例子中使用了两个读线程和一个写线程,代码如下:


程序的执行效果如下:

WaitHandle
WaitHandle类是一个抽线类,有多个类直接或者间接继承自WaitHandle类,类图如下:

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

程序的运行结果如下:
Result = 0.355650523270459.
Result = 0.125205692112756.
当然因为引入了随机数,所以每次计算结果并不相同,这里要讲述的是它们之间的控制。首先在 Result(int seed)方法中讲计算基数、第一项、第二项及第三项的方法放到线程池中,要计算第一二三项时首先要确定基数,这些方法通过manualEvent.WaitOne()暂时停止执行,于是计算基数的方法首先执行,计算出基数之后通过manualEvent.Set()方法通知计算第一二三项的方法开始,在这些方法完成计算之后通过autoEvents数组中的AutoResetEvent元素的Set()方法发出信号,标识执行完毕。这样WaitHandle.WaitAll(autoEvents)这一步可以继续执行,从而得到执行结果。

在上面代码中的WaitHandle的其它子类限于篇幅不在这里一一举例讲解,它们在使用了多少有些相似之处(毕竟是一个爹、从一个抽象类继承下来的嘛)。

图片上传功能暂时关闭,敬请谅解。

分享到:
评论

相关推荐

    Linux多线程服务端编程:使用muduo C++网络库

    本书主要讲述采用现代C++ 在x86-64 Linux 上编写多线程TCP 网络服务程序的主流常规技术,重点讲解一种适应性较强的多线程服务器的编程模型,即one loop per thread。这是在Linux 下以native 语言编写用户态高性能...

    Linux多线程服务端编程-使用muduo C++网络库

    《Linux多线程服务端编程:使用muduo C++网络库》主要讲述采用现代C++在x86-64 Linux上编写多线程TCP网络服务程序的主流常规技术,重点讲解一种适应性较强的多线程服务器的编程模型,即one loop per thread。...

    解决多线程编程中的同步互斥问题

    解决多线程编程中的同步互斥问题

    Java多线程编程总结

    Java线程:线程的同步-同步方法 Java线程:线程的同步-同步块 Java线程:并发协作-生产者消费者模型 Java线程:并发协作-死锁 Java线程:volatile关键字 Java线程:新特征-线程池 Java线程:新特征-有返回值的...

    java多线程编程总结

    详细的讲述了多线程的各种用法 Java线程:概念与原理 Java线程:创建与启动 Java线程:线程栈模型与线程的变量 Java线程:线程状态的转换 Java线程:线程的同步与锁 Java线程:线程的交互 Java线程:线程的调度-休眠...

    多线程代码 经典线程同步互斥问题 生产者消费者问题

    c: 多线程访问同一资源 d: 经典线程同步互斥问题 e: 使用关键段解决子线程互斥问题 f: 利用事件实现线程同步问题 g: 利用互斥量来解决线程同步互斥问题 h: problem1 生产者消费者问题 (1生产者 1消费者 1...

    C++面向对象多线程编程

    《C++面向对象多线程编程》共分13章,全面讲解构建多线程架构与增量多线程编程技术。第1章介绍了用于构建面向对象程序的不同类型C++组件,以及如何使用这些组件来构建多线程架构。第2、3、4章简要介绍进程、线程、多...

    java并发编程:线程基础

    本资源致力于向您介绍 Java 并发编程中的线程基础,涵盖了多线程编程的核心概念、线程的创建和管理,以及线程间通信的基本方法。通过深入学习,您将建立扎实的多线程编程基础,能够更好地理解和应用多线程编程。 多...

    Linux多线程服务端编程:使用muduo C++ 网络库.陈硕(详细书签)

    本书不仅讲解了创建并行代码时会遇到的问题,而且还帮助读者更好地理解同步技术。本书的目标是帮助读者在理解多线程编程概念的同时,能快速有效地进行并行计算和并发编程

    C++11中多线程编程-std::async的深入讲解

    C++11中提供了异步线程接口std::async,std::async是异步编程的高级封装,相对于直接使用std::thread,std::async的优势在于: 1、std::async会自动创建线程去调用线程函数,相对于低层次的std::thread,使用起来非常...

    实验二、嵌入式Linux多线程编程实验

    2. 掌握如何利用信号量完成线程间的同步与互斥。 3. 熟悉Makefile工作原理,掌握编写Makefile的编写方法。 二、实验基本要求 1. 掌握熟悉线程的定义及操作方法。 2. 利用信号量的PV操作完成完成以下单个生产者和单个...

    Windows多线程编程(pdf)

    多线程编程之一——问题提出 2 一、问题的提出 2 二、多线程概述 2 三、Win32 API对多线程编程的支持 3 四、Win32 API多线程编程例程 4 例程1 MultiThread1 4 例程2 MultiThread2 5 例程3 MultiThread3 6 例程4 ...

    MFC多线程编程实例

    内含9个MFC多线程编程实例,包括Wind32多线程编程、线程间通讯以及线程同步的临界区、事件、互斥量和信号量等。

    Linux多线程服务端编程 陈硕

    本书主要讲述采用现代 C++ 在 x86-64 Linux 上编写多线程 TCP 网络服务程序的主流常规技术,重点讲解一种适应性较强的多线程服务器的编程模型,即 one loop per thread。这是在 Linux 下以 native 语言编写用户态高...

    Python多线程编程

    Python多线程编程文档说明 多进程编程 一、 multiprocessing 模块 1. multiprocessing 模块提供了多进程编程的能力 它的API非常类似于 threading 模块,但是也提供了一些threading 模块不具有的能力 相比于线程,它...

    windows下多核多线程编程

    示例代码展示了如果在windows实现多线程的同步,如何在多核CPU上开辟多个线程同时运行。

    Linux多线程编程手册

    本手册详细介绍Linux多线程编程同步技术,可供广大linux工程师参考

    Window下多线程编程、Linux下多线程编程-多线程实验bistu

    (2)基于Beginthread实现多线程,并实现线程的简单同步。 (3)实现多线程字符串查找程序 Linux: (1)实现多线程字符输出,存在三个线程,线程1用来监听用户输入和其他两个线程的监听器,线程2的主要内容为每隔一...

Global site tag (gtag.js) - Google Analytics