线程同步

2024-07-30

线程同步(精选5篇)

线程同步 篇1

假如所有的线程都能够独自运行并且不需要相互通信的时候, Linux、Unix、windows、Mac OS等等操作系统都能进入最佳运行状态, 但是很少有线程能够总是独自运行。首先创建线程是为了执行具体任务, 当任务完成以后, 要通知到别的一个线程或者进程;其次系统资源包括堆、串口、文件、窗口以及无数其他资源, 多个进程或线程访问共享的资源, 不能破坏资源的完整性, 从这两方面说起, 就需要引入线程同步技术。

1 线程同步两种模式

线程的同步技术主要包括两个方面的内容:用户模式下的线程同步、内核对象下的线程同步。用户模式下的线程同步主要包括以下几种方式:volatile类型限定符、原子访问, 关键段, Slim读写锁等等, 其中最常用是关键段技术。内核对象下的线程同步:主要包括等待函数、事件内核对象、信号量互斥对象、互斥量内核对象等等[1]。

用户模式下的线程同步不涉及CPU模式切换, 因此执行速度较快, 但是仅限于单个进程间的线程同步。内核对象下的线程同步会涉及到CPU模式切换, 消耗时间片较多, 但是支持多个进程间的同步[2]。

2 用户模式下的线程同步

2.1 关键段线程同步

关键段 (Critical Section) 是一小段代码[3], 关键段在执行之前独占一些共享资源的访问权。这种方式可以让多行代码以“原子方式”对资源进行操控。这里的原子方式, 指的是代码知道除了当前线程之外, 没有其他任何线程会同时访问该资源。当然, 系统仍然可以暂停当前线程去调度其他线程。但是, 在当前线程离开关键段之前, 系统是不会去调度任何想要访问同一资源的其他线程。

2.2 关键段工作原理

Enter Critical Section函数会检查关键段中的某些成员变量, 这些成员变量表示是否有线程正在访问资源:如果没有线程正在访问资源, 那么Enter Critical Section会更新成员变量, 以表示调用线程已经获准对资源的访问, 并立即返回, 这样线程就可以继续执行。如果成员变量表示调用线程已经获准访问资源, 那么Enter Critical Section会更新变量, 以表示调用线程被获准访问的次数。如果成员变量表示其他线程已经获准访问资源, 那么Enter Critical Section会使用一个事件内核对象把当前线程切换到等待状态。关键段的核心价值在于它能够以原子的方式执行所有这些测试。

2.3 关键段线程同步的特点

关键代码段最大的优点就是在用户模式执行线程同步, 不需要进入内核模式, 从而减少了进入内核模式所用的时间;关键段不是内核对象, 所以只能用来同一进程内线程间的同步, 不能用来多个不同进程间的线程的同步, 因此要比其他内核对象的速度要快[4]。

3 Windows平台关键段的实现

假如没有上面的Enter Critical Section和Leave Critical Section, 当两个线程函数分别在两个线程中执行的时候, g_n Sum的状态是不可预计的。

在上面的代码中, 首先定义了一个叫g_cs的CRITICAL_SEC TION数据结构, 然后把任何需要访问共享资源 (这里的g_n Sum) 的代码放在Enter Critical Section和Leave Critical Section之间。这里需要注意的是, 关键段需要用在所有的相关线程中 (即:上面的两个线程函数都要放在关键段中) , 否则共享资源还是有可能被破坏。另外, 在调用Enter Critical Section函数之前需要调用Initialize Critical Section函数初始化关键段, 当不需要访问共享资源的时候, 应该调用Delete Critical Section函数来释放关键段资源。

4 结语

了解关键段同步技术的工作原理以及工作特点, 运用VS开发工作使用C++语言, 编写了在Windows平台实现关键段的同步代码, 程序运行测试达到了一个进程间的线程同步的目的。

摘要:文章阐述了多线程需要同步的原因, 比较了用户模式下的线程同步和内核对象下的线程同步两种同步机制的优缺点, 详细阐述了用户模式下的关键段同步技术、工作原理以及关键段同步的特点, 在windows平台下实现了使用关键段技术作为同步的代码。

关键词:关键段,多线程同步,windows代码

参考文献

[1]吕浩勇, 余启港, 董元和.windwos多线程同步技术研究[J].计算机与现代化, 2006 (10) :86-89.

[2]王义涛, 张莉.一种多线程同步防死锁算法研究[J].计算机与数字工程, 2007, 35 (8) :14-16, 40.

[3]王日宏.基于VC的Win32多线程同步问题[J].计算机系统应用, 2004 (7) :60-62.

[4]许斌龙, 张晶, 王国明, 等.Win32环境下的多线程同步技术的研究[J].计算机技术与发展, 2013 (12) :26-29.

线程同步 篇2

3、线程睡眠 sleep

所有介绍多线程开发的学习案例中,基本都有用到这个方法,这个方法的意思就是睡眠(是真的,请相信我...),好吧,如果你觉得不够具体,可以认为是让当前线程暂停一下,当前线程随之进入阻塞状态,当睡眠时间结束后,当前线程重新进入就绪状态,开始新一轮的抢占计划!

那么这个方法在实际开发中,有哪些用途呢?我举个例子,很多情况下,当前线程并不需要实时的监控或者是运行,只是会定期的检查一下某个状态是否达标,如果符合出发条件了,那么就做某一件事情,否则继续睡眠。比如心跳模式下,我们会派一个守护线程向服务端发送数据请求,当收到回应时,那么我们会睡眠一段时间,当再次苏醒后,我们继续发送这样的请求。现实生活中的例子,比如我们在等某个电视是否开播,可是又不想看之前的广告,所以我们可能会等一会将电视频道切换到要播放的位置查看一下,如果还在播放广告,那么我们就跳到其他频道观看,然后定期的切换到目标频道进行查看一下。

代码如下:

1  public class ThreadStudy

2  {

3    public static main(String[] arg)throws Exception

4    {

5      for(int i=0;i<=1000;i++)

6      {

7        if(IsInternetAccess())

8        {

9          Thread.sleep(1000*6);//注意这里

10        }

11        else

12        {

13          System.out.println(“Error! Can not Access Internet!”)

14          break;

15        }

16      }

17    }

18    private Boolean IsInternetAccess()

19    {

20      //bala bala

21      return true;

22    }

23  }

代码的意思是检查网络是否通畅,如果通畅的话那么进入睡眠,睡眠6秒钟后再次苏醒进行一次检查。通过让线程睡眠,我们可以有效的分配资源,在闲时让其他线程可以更快的拿到cpu资源。这里有一点需要注意的是,线程睡眠后,进入阻塞状态(无论此时cpu是否空闲,都仍然会暂停,是强制性的),当睡眠时间结束,进入的是就绪状态,需要再次竞争才可以抢占到cpu权限,而非睡眠结束后立即可以执行方法。所以实际间隔时间是大于等于睡眠时间的。

java Thread类提供了两个静态方法来暂停线程

1 static void sleep(long millis)

2

3 static void sleep(long millis,int nanos)

millis为毫秒,nanos为微秒,与线程join()类似,由于jvm和硬件的缘故,我们也基本只用方法1。

4、 线程让步 yield()

在生活中我们都遇到过这样的例子,在公交车、地铁上作一名安静的美男子(或者是女汉子),这时候进来了一位老人、孕妇等,你默默的站起来,将座位让给了老人。自己去旁边候着,等着新的空闲座位。或者是你默默的玩着电脑游戏,然后你妈妈大声的喊你的全名(是的,是全名),这时候你第一反应是,我又做错什么了,第二反应就是放下手上的鼠标,乖乖的跑到你老妈面前接受训斥。所有的这一切都是由于事情的紧急性当前正在处理的线程被搁置起来,我们(cpu)处理当前的紧急事务。在软件开发中,也有类似的场景,比如一条线程处理的任务过大,其他线程始终无法抢占到资源,这时候我们就要主动的进行让步,给其他线程一个公平抢占的机会。

这里附加一份来自网络的图片:在我们强大的时候,我们应该给弱者一个机会。咳咳 回归正题。

下面是代码

1 public class TestThread extends Thead

2 {www.2cto.com

3  public testThread(String name)

4  {

5    super(name);

6  }

7

8  public void run()

9  {

10    for(int i=0;i<=1000000;ii++)

11    {

12      send(“MsgBody”);

13      if(i%100==0)

14      {

15        Thread.yield();//注意看这里

16      }

17    }

18  }

19

20  public static void main(String[] args) throws Exception

21  {

22    TestThread thread1=new TestThread(“thread1”);

23    thread1.setPriority(Thread.MAX_PRIORITY);//注意看这里

24

25    TestThread thread2=new TestThread(“thread2”);

26    thread1.setPriority(Thread.MIN_PRIORITY);//注意看这里

27    thread1.start();

28    thread2.start();

29  }

30 }

我们启动线程后,当线程每发送一百次消息后,我们暂停一次当前线程,使当前线程进入就绪状态,

此时CPU会重新计算一次优先级,选择优先级较高者启动。

此处比较一下 sleep方法和yield()方法。

(1)sleep方法 暂停线程后,线程会进入阻塞状态(即使是一瞬间),那么在这一刻cpu只会选择已经做好就绪状态的线程,故不会选择当前正在睡眠的线程。(即使没有其他可用线程)。而yield()方法会使当前线程即刻起进入就绪状态,cpu选择的可选线程范围中,包含当前执行yield()方法的线程。如若没有其他线程的优先级高于(或者等于) yield()的线程,则cpu仍会选择原有yield()的线程重新启动。

(2)sleep方法会抛出 InterruptedException 异常,所以调用sleep方法需要声明或捕捉该异常(比C#处理异常而言是够麻烦的),而yield没有声明抛出异常。

(3)sleep方法的移植性较好,可以对应很多平台的底层方法,所以用sleep()的地方要多余yield()的地方;

(4)sleep 暂停线程后,线程会睡眠 一定时间,然后才会变为就绪状态,倘若定义为sleep(0)后,则阻塞状态的时间为0,即刻进入就绪状态,这种用法与yield()的用法基本上是相同的:即都是让cpu进行一次新的选择,避免由于当前线程过度的霸占cpu,造成程序假死。

这两个方法最大的不同点是 sleep会抛出异常需要处理,yield()不会; 而且两者的微小区别在各个版本的jdk中也不一样,大家看以参阅stackoverflow上的这个问题:Are Thread.sleep(0) and Thread.yield() statements equivalent?(点此进入)

5、线程的优先级设定

线程的优先级相当于是一个机会的权重,优先级高时,获得执行机会的可能性就越大,反之获得执行机会的可能性就越小。(记住只是可能性越大或越小)。

在本节的线程让步这一部分的代码里我们已经用代码展示了如何设置线程的优先级此处不做特别的代码展示。

Thread为我们提供了两个方法来分别设置和获取线程的优先级。

1 setPriority(int newPriority)

2 getPriority()

setPriority为设置优先级,参数的取值范围是 1~10之前。

同时还设定了三个静态常量:

Tread.MAX_PRIORITY=10;

Tread.NORM_PRIORITY=5;

Tread.MIN_PRIORITY=1;

尽管java为线程提供了10个优先级,但是底层平台线程的优先级往往并不为10,所以就导致了两者不是意义对应的关系。(比如OS只有五个优先级,这样每两个优先级只对应一个OS的优先级)。 此时我们常常只用这三个静态常量来设置优先级,而不是详细的指明具体的优先级值(因为可能多个优先级对应OS的某一个优先级),造成不必要的麻烦。

另外每个线程默认的优先级都与创建他的父进程的优先级相同,在默认情况下Main线程优先级为普通,所以上述代码创建的新线程默认也为普通优先级。

下面是优先级概念的重点:

其实你设置的优先级并不能真正代表该线程的或者启动的优先级,这只是OS启动线程时计算优先级的一个参考指标。OS还会查看当前线程是否长时间的霸占cpu,如果是这样的话,OS会适度的调高对其它“饥饿”线程的优先级。对于那些长期霸占cpu的线程进行强制的挂起。进行这种设置只是能在某种程度上增加该线程被执行的机会。其实那些长期霸占cpu的线程也并非单次霸占的时间长,而是被连续选中的情况非常多,造成一种长期霸占的假象。

所以设置优先级后,线程真正执行的顺序并不可以预测甚至可以说是有点混乱的。在明白了这点以后,我们在开发控制多线程,并不能完全的寄希望于通过简单的设置优先级来安排线程的执行顺序。

此处参考了两篇文章,更多详情请参考原文:

(1)Java多线程 -- 线程的优先级(原文链接)

(2)Thread.sleep(0)的意义(原文链接)

6、强制结束线程Stop()

有时我们会发现有些正在运行的线程,已经没有必要继续执行下去了,但是距离多线程结束还有一段时间,这时我们就需要强制结束多线程。java曾经提供过一个专门用于结束线程的方法Stop(),但是这个方法现在已经被废弃掉了,并不推荐开发者使用。

这是由于这个方法具有固有的不安全性。用Thread.stop 来结束线程,jvm会强制释放它锁定的所有对象。当某一时刻对象的状态并不一致时(正在处理事务的过程中),如果强制释放掉对象,则可能会导致很多意想不到的后果。说的具体一点就是:系统会以被锁定资源的栈顶产生一个ThreadDeath异常。这个unchecked Exception 会默默的关闭掉相关的线程。此时对象内部的数据可能会不一致,而用户并不会收到任何对象不一致的报警。这个不一致的后果只会在未来使用过程中才会被发现,此时已经造成了无法预料的后果。

有些人可能会考虑通过调用Stop方法,然后再捕捉ThreadDeath的形式,避免这种形式。这种想法看似可以实现,其实由于ThreadDeath这个异常可能在任何位置抛出,需要及细致的考虑。而且即使考虑到了,在捕捉处理该异常时,系统可能又会抛出新的ThreadDeath。所以我们应该在源头上就扼杀掉这种方式,而不是通过不断的打补丁来修复。

那么问题来了,如果我们真的要关闭掉某个线程,应该怎么处理呢?

通过Stop方法的讲解我们可以明白,在线程的外部来关闭线程往往很难处理好数据一致性、以及线程内部运行过程的问题。那么我们可以通过设定一直标志变量,然后线程定期的检查这个变量是否为结束标识来确定是否继续运行。

例如笔者曾经写过一个监控计算机指标的线程。这个线程会定期的检查缓存中的状态变量。这个状态缓存是外部可以设定的。当线程发现此变量已经被设定为“结束”时,则会在内部处理好剩余工作,直接运行完Run方法。

7、线程的挂起和恢复 suspend()和resume()

我们有时需要对线程进行挂起,而具体挂起的时间并不清楚,只可能在未来某个条件下,通知这个线程可以开始工作了。java为我们专门提供了这样的两个方法:

挂起 suspend()/恢复resume。

通过标题我们已经知道这两个方法也同样不被java所推荐,但是为什么会这样呢?

suspend是直接挂起当前线程,使其进入阻塞状态,而对他内部控制和锁定的资源并不进行修改(这与stop方法类似,线程外部往往很难查看内部运行的状态和控制的资源,所以也就很难处理)。这样这个被挂起的线程所锁定的资源就再也不能被其他资源所访问,造成了一种假死锁的状态。只有当线程被恢复(resume)后,并且释放掉手里的资源,其他线程才可以重新访问资源,但是倘若其他线程在恢复(resume)被挂起(suspend)的线程直线,需要先访问被锁定的资源,此时就会形成真正的锁定。

那么问题来了,如果我们真的要挂起某个线程,应该怎么处理呢?

这个与stop()同理,我们可以在可能被挂起的线程内部设置一个标识,指出这个线程当前是否要被挂起,若变量指示要挂起,则使用wait()命令让其进入等待状态,若标识指出可以恢复线程时,则用notify()重新唤醒这个线程。(这两个方法我会在后文的线程通信中讲解)。

此处参考了两篇文章,更多详情请参考原文:

(1)为何不赞成使用Thread.stopsuspend和resume()(原文链接)

线程同步 篇3

对于Java语言编程机制而言,当它与同步线程模型进行了一定程度上的结合之后,能够对其系统并发执行程度进行较大幅度的提高,也就是说在原先的编程机制基础之上进一步的发展与提高。之前,在原先的Java编程机制之中存在着一定的弊端与局限性,这些问题的存在阻碍了Java编程机制的进一步发展,然而,自从在Java之中引入了同步线程模型之后,原先所存在的问题就能够得到一定程度上的控制。我们将同步线程模型与多进程模式进行一定程度上的对比分析,发现相比于多进程模式,前者能够发挥出更大的优越性,两者之间的差异性也能很明显的表现出来。两者之间的差异性主要表现在如下几个方面:对于进程而言,它的两个单位之间是相互分开的,而对于线程而言,它的每个执行单元具有一定程度的独立性,从客观的角度来说,实现独立调整已经不是问题,而是成为了一种非常熟练的工作。另一方面,通过技术上进步,各自分派也成为可能,每个进程都可以称之为独立出来的控制流,虽然在一定程度上具有了独立性,但从实质上来看,它并不是可独立拥有资源的基本单位。从这方面来看,在实际的操作当中,能够实现对独立拥有资源的单位频率进行切换,而且操作频率有所降低。进程并发的执行存在着两个十分必要的条件,这两个必要的条件分别害死可拥有资源的独立单位以及可独立调度及分派的基本单位。在传统的操作系统之中,这两项属性得到了切实体现。值得注意的是,总体进程之中,两种属性成为了一种非常坚实的基础,为进程的并发执行提供了强大的动力。还有就是,为了全面提高程序并发执行的有效性,有一部分的操作仍然只能够在系统中进行实时,这些操作主要有创建以及撤销等。对于一项资源的拥有者而言,在资源之中,对于不同的进程而言,它所具有的内存空间堆栈等也存在着一定程度上的差异。基于此,当系统在对这一系列的操作进行执行时,必须将这一情况纳入到考虑范围之内,腾出更大的空间。所以,在对系统进行设置时,对于进程数目的设定要求能够满足相应的标准,在对进程进行切换时也应该控制好次数。也就是说,从这一方面来看,这对进程并发更深层次的发展造成了一定程度上的阻碍作用。然而,对于Java同步线程模式而言,其难点主要集中在如何采取有效措施对并发程序设计中所出现的一系列问题进行合理而有效的解决。

一般情况下,在进行操作的过程之中,可以对线程进行一定程度的联想,将线程当做是程序当中的某个控制流程。如果是从固有系统角度出发,多数情况仅仅会存在和一个程度的控制流程,且这种控制流程具有相应的循环性,因此在原先的操作系统之后总,对于流程的执行职能按部就班,一步一步来完成,这种环境我们可以将之称作为单线程,无疑单线程的执行效率是相对较低的。近几年来,随着科学技术水平的不断进步,在原先的操作系统的基础之上引进了新技术,取得了较大程度上的进步,Java多线程模式便是其中一种典型技术。随着多线程模式的引入,原先的单线程模式也逐步淡出人们的视线。新型的Java多线程模式可以大幅提高执行效率,它不仅可以对程序的执行进行有效的支持,同时又能够对多个控制流程进行流畅操作。

一般情况下,在对Java多线程模式进行操作的过程之中,会出现两条控制线程,且这两条控制线程具有一定的特殊性,这两条控制线程存在于操作程序之中,且两者所执行的任务不同,一条线程的职责是对用户事件进行一定程度上的处理,而另一条线程作用是进行分析运算。值得注意的是,一般情况下处理器仅仅只有一个个时,也就是说线程是难以进行同时处理的,两条线程的执行之间必须存在着一定的空隙,如果其中一条线程正在等待对相关数据进行获取时,则系统就会自动进行线程的切换。正是由于这一原因,在对多线程模式进行引入之后,需要与计算机处理器个数组合。

2Java同步进程模型改进的尝试

对于Java编程语言而言,从整体角度上来看,线程模型正是其“软肋”,因为Java编程语言中的线程模型与实际情况的适应度较低,而且它所面对的对象相对狭窄,不能做到面面俱到。除此之外,Java语言的优势在于,其线程模型的满意度较高,能够实现很多的功能,减少了客观上的影响。从综合的角度来分析,Java作为一种编程语言,其线程模型仍然存在着诸多的缺陷,这些缺陷的存在阻碍了其进一步的发展。因此,采取有效措施对Java同步进程模型的改进十分重要。

而要想有效解决Java同步线程模型所存在的额缺陷,处理数据死锁是重中之重,就死锁本身来说,它的形成,主要是两个或者是两个以上的线程,互相之间的无限制等待情况,最终结果就是,两个线程都没有办法执行任务,那么这一状态就被称作为死锁状态。在Java同步线程之中,为了应对这一问题,通过对synchronized关键词进行一定程度上的使用,并在此基础之上实现对于对象的锁操作。当锁操作完成之后,之前所运用的synchronized关键词语句便能够得到有效的执行,除了以上的阐述之外,语句执行结束后,无论系统判定为正常也好、不正常也好,解锁操作都会自动完成。

一般情况下,对于并发线程而言,它们会对程序中的资源进行一定程度上的竞争,针对这一情况,必须采取措施对程序当中的共享资源进行有效而均衡的分配,从而达到线程在执行程序的过程之中能够对有限的资源进行充分利用的效果。在Java之中,并不存在专门为死锁提供检测的程序。所以,对于Java程序员而言,就必须认真处理死锁的相关程序。一般情况下,对于线程程序而言,很多都是可以对其进行归纳总结的,如果两个或两个以上的线程彼此之间无限期的等待,最终导致两个线程都无法有效的执行任务,从而造成死锁。比如如下程序:

从以上的编程来看,不难发现,一个对象会对应另一个与其存在一定关联的monitor对象,这个对象的具体作用可以用“守门人”来形容,每次仅仅允许一个synchronized方法进入,而如果其中的一个synchronized方法结束之后,monitor就会进行一定程度上的解锁,此时另外一个synchronized方法便可以开始执行。那么应该如何采取有效措施对这一死锁问题进行解决?其实只需要对正确线程程序进行编制,主要策略如下:首先,需要对给定的任务目标进行有效的完成,在执行目标的过程中,需要注意一定的同步性,即与其他任务同时进行,通过这样的操作,可以对每个线程完成的功能进行一定程度上的控制,并在此基础之上,就没有必要对两个线程进行使用,因为在这一操作之后,两者之间就存在着一定的依赖性,当从一个synchronized方法中调用另一个syn-chronized方法时,要谨慎方可。

3结束语

随着计算机技术的逐渐发展,Java语言编程的效果已经越来越明显,将其引入同步线程模型,可以充分改进其中的一些弊端,将大大提高运行效率。

摘要:目前状况下,随着经济的迅速发展以及科学技术水平的不断提高,我国的计算机科学技术取得了较大程度上的发展,为我国国民经济的提高做出重要贡献。众所周知,计算机科学技术的发展与语言编程机制的逐步发展与完善莫不可分。Java语言编程机制便是其中的重要一种,随着它的不断发展与改进,目前状况下它已经在计算机语言中有了十分广泛的运用。该文主要针对Java同步线程模型分析与改进进行研究与分析。

线程同步 篇4

为了提高计算机系统的资源利用率和执行效率, 在现代操作系统的进程中, 往往又创建多个线程, 并使这多个线程并发执行。

进程中的线程间之间可能由于需要协同操作来完成一个共同的任务, 故它们之间表现的是具有直接的相互制约关系; 由于共享资源而引起的线程间的关系称为间接相互制约关系。 因此系统中的线程间存在着彼此相互影响而处于复杂的环境中。

在系统中, 若所涉及的线程大多都是独立的, 而且异步执行, 即是说每个线程都包含了运行时自身所需要的数据或方法, 而无需外部的资源或方法, 也不必关心其他线程的状态或行为, 则无需采用何种手段来解决这些线程间的关系问题。 但是, 在进行多线程的程序设计中, 一种情况是需要实现多个线程合作来完成某项任务时, 这时需要解决这一组线程的执行次序问题; 另一种情况是要实现共享临界资源的合作线程的执行次序问题。 此时, 则需使用线程的同步机制, 以保证这些并发线程间的同步, 否则, 就会产生与时间有关的错误。 这是人们经历过多少次失败的实践而总结出了理论依据, 为人们进行程序设计提供了宝贵的理论规则, 要切记严格遵守。

2线程的同步

在操作系统中, 当进程中某一线程正在修改某一存储区域(或变量) 的内容的时候, 就不允许其他线程来读出或修改该存储区(或变量) 的内容, 否则就会发生无法估计的错误, 线程间的这种相互制约关系称为互斥。 互斥即是并发线程对临界资源(或不能被共享的资源) 的访问的互相排斥。

并发线程按各自的速度在向前推进, 但在一些关键点上, 可能需要互相等候与互通消息, 以实现它们之间各操作之间的次序, 这种相互制约的等候与互通消息被认为线程程同步。 线程同步即是对线程操作的时间顺序所施加的某种限制。

线程的同步是要实现各并发线程的操作之间的次序, 而互斥所遵循的规则只是两个操作不能在同一个时刻发生, 主要用来解决对临界资源的使用问题。 故而互斥是一种比较特殊的同步。

3线程同步的实现方式

为了避免彼此有关系的并发线程的执行时发生与时间有关的错误, 必须要引入同步机制。 线程的同步方式有很多, 现介绍两种常用的方式。

3.1利用事件对象

事件对象也属于内核对象, 包含一个使用计数, 一个用于指明该事件是一个自动重置的事件还是人工重置的事件的布尔值, 另一个用于指明该事件处于激发状态还是未激发状态, 也称有信号状态还是无信号状态的布尔值。

人工重置事件被设置为激发状态后, 会唤醒所有等待的线程, 而且一直保持为激发状态, 直到程序重新把它设置为未激发状态; 而自动重置事件被设置为激发状态后, 会唤醒 “一个” 等待中的线程, 然后自动恢复为未激发状态。 用自动重置事件来同步两个线程比较理想。

3.2利用关键代码段

关键代码段也叫临界区, 它工作在用户方式下。 关键代码段是指一个小代码段, 在代码能够执行前, 它必须独占对某些资源的访问权。

关键代码段非常类似于公用电话亭。 当想进入公用电话亭去访问电话这种资源的时候, 首先要判断一下电话亭中是否有人, 如果已经有人在使用这种资源, 就只能在公用电话亭外面等候, 当这个人访问完电话这种资源, 才能够进入到公用电话亭, 去访问电话这种资源。

当多个线程访问一个独占性共享资源时, 可以使用临界区对象。 拥有临界区的线程可以访问被保护起来的资源或代码段, 其他线程若想访问, 则被挂起, 直到拥有临界区的线程放弃临界区为止。

4线程同步的编程举例

现以单处理机的环境为例, 简单介绍上述两种常用的方式用C系列语言的编程实例。

模拟实现汽车站售票系统。 设有两个售票窗口同时出售同一个车次的共有100张的汽车票, 要求正确地实现其售票工作。

在VC6.0的环境中建立名为Multi Thread的一个Win32控制台应用程序的工程, 然后建立与工程同名的C++源文件。 在该源文件中, 编写3个函数: 主线程函数main() 及另外两个线程函数Fun1Proc() 和Fun2Proc()。 在主线程函数中, 先定义两个HANDLE类型的变量h Thread1和h Thread2作为线程的句柄; 然后调用Create Thread函数分别创建两个线程, 接下来调用Close Handle关闭两个线程的句柄。 在主线程的最后, 调用Sleep这个函数, 为其实参传递一个4000的值, 让主线程睡眠4秒钟, 当主线程执行到该函数调用语句时就放弃了它执行的权利, 以使另两个线程可得到执行权。

在另外两个售票线程Fun1Proc() 和Fun2Proc() 中, 其执行代码相似, 都使用while循环进行售票, 在while的循环体中有一个语句 “if…else…”, 用于判断, 如果tickets大于0, 则去销售车票, 并且将所卖的票号打印输出; 否则如果tickets等于0, 说明票已经售完, 则用break来终止循环。 这个 “if …else…” 语句即为访问临界资源(tickets) 的临界区。

(1) 用事件对象实现售票线程间同步的编程

在编写程序代码时, 在主线程的关闭线程句柄的语句之后, 再调用Create Event函数创建一个事件内核对象。 Cre- ate Event函数的第2个参数是一个布尔类型的值, 它是用来指定是人工重置还是自动重置的事件对象被创建; 如果为TRUE, 则是人工重置的事件对象被创建, 必须使用Re- set Event函数来人工重置这个事件对象状态为未激发状态; 如果为FALSE, 当一个等待线程被释放后, 系统将会自动将事件对象重置为未激发状态。 将函数Create Event的第2个参数设置为FALSE。

在每一个线程的代码中, 在进入临界区之前, 都用Wait- For Single Object函数去请求事件对象, 一旦得到事件对象之后, 则可以进入到临界区中。

要想让每个线程能够得到事件对象, 必须将这个事件对象设置为激发状态, 可以在调用Create Event创建事件对象的时候将其第3个参数传递为TRUE, 这样所创建的事件对象初始化状态就是激发状态, 否则, 事件对象的初始状态为未激发状态。 但是在用Create Event创建事件对象的时候如将其第3个参数传递为FALSE, 也可以通过调用Set Event函数设置指定的事件对象为激发状态。

在每一个线程的代码中, 在退出临界区之后前, 都调用Set Event函数将事件对象设置为激发状态。 其完整代码如下:

(2) 用关键代码段实现售票线程间同步的编程

利用关键代码段实现线程间同步时, 删除上面程序代码中的斜线语句代码。 在主线程函数之前, 要用CRITI- CAL_SECTION定义一个全局的临界区对象g_cs。 接下来在主线程函数中的Close Handle关闭两个线程的句柄的语句之后要调用Initialize Critical Section (&g_cs) 函数调用去初始化这个临界区对象, 经过这样的步骤相当于先把电话亭搭建好。 当程序要退出的时候, 即在主线程函数的代码的最后要调用Delete Critical Section (&g_cs) 函数删除临界区对象并释放由该对象使用的所有系统资源。

线程同步 篇5

引入线程的目的是为了支持多线程程序设计,即在一个程序中创建了多个线程。在多线程的程序中,当多个线程并发执行时,虽然各个线程中语句的执行顺序是确定的,但线程的相对执行顺序是不确定的,在多个线程需要共享。共享存储结构时这种执行顺序的不确定性可能会产生执行结果的不确定性,甚至可能造成程序出现错误。本文主要讨论如何控制互相交互的线程之间的运行进度,使线程执行时不出现错误结果,即线程间的同步。

2 线程同步

合理地同步一个程序是最精细的软件开发任务之一。在深入到细节之前,应该首先确认使用同步是否不可避免。应该意识到,对程序中资源的访问进行同步时,其难点来自于是使用细粒度锁还是粗粒度锁这个两难的选择。如果采用粗粒度的同步方式,虽然可以简化代码,但也会把自己暴露在争用瓶颈的问题上。因此在开始谈论有关同步机制之前,有必要先了解一下竞态条件和死锁。

2.1 竞态条件

竞态条件指的是一种特殊的情况,在这种情况下各个执行单元以一种没有逻辑的顺序执行动作,从而导致意想不到的结果。举例,线程T修改资源R后,释放了它对R的写访问权,之后又重新夺回R的读访问权再使用它,并以为它的状态仍然保持在它释放它之后的状态。但是在写访问权释放后到重新夺回读访问权的这段时间间隔中,可能另一个线程已经修改了R的状态。

2.2 死锁

死锁指的是由于两个或多个执行单元之间相互等待对方结束而引起阻塞的情况。例如:

1)一个线程T1获得了对资源R1的访问权。2)一个线程T2获得了对资源R2的访问权。3)T1请求对R2的访问权但是由于此权力被T2所占而不得不等待。4)T2请求对R1的访问权但是由于此权力被T1所占而不得不等待。T1和T2将永远维持等待状态,此时我们陷入了死锁的处境,针对此问题主要有三种解决方案:

1)在同一时刻不允许一个线程访问多个资源。2)为所有访问资源的请求系统地定义一个最大等待时间(超时时间),并妥善处理请求失败的情况。几乎所有的.NET的同步机制都提供了这个功能。前两种技术效率更高但是也更加难于实现。事实上,它们都需要很强的约束,而这点随着应用程序的演变将越来越难以维护。大的项目通常使用第三种方法。

3 线程同步主要实现方法

3.1 用lock语句实现互斥

Lock语句的形式如下:lock(e){访问共享资源的代码},这对{}内部的代码就是要锁定的代码(即临界区)

1)其中e表示要锁定的对象,锁定该对象内所有临界区,必须是引用类型,一般为this。

2)Lock语句将访问共享资源的代码标记为临界区。临界区的意义是:假设线程1正在执行e对象的(某一个)临界区中的代码时,如其他线程也要求执行这个e对象的任何临界区中代码,将被阻塞,一直到线程1退出临界区。

3)一个对象e临界区可以具有多个。把访问共享资源的代码放在临界区中。

4)当某个线程(比如线程1执行的代码)进入到某个对象的一个临界区(代码)时,则其他线程都不能进入该对象的任何一个临界区中去执行;直到线程1(执行的代码的位置)离开该对象的临界区以后,其他线程才可进入该对象的临界区(去执行代码)。

3.2 用Mutex类实现互斥

可以使用Mutex类对象保护共享资源(如上例中的总人数变量)不被多个线程同时访问。

Mutex类WaitOne方法和ReleaseMutex方法之间代码是互斥体,这些代码要访问共享资源。Mutex类对象的WaitOne方法分配互斥体访问权,该方法只向一个线程授予对互斥体的独占访问权。如果一个线程获取了互斥体,则要获取该互斥体的第二个线程将被挂起,直到第一个线程用ReleaseMutex方法释放该互斥体。这样通过把访问共享资源的代码放在Mutex类对象的互斥体中,就可以避免多个线程同时执行会访问共享资源的代码。

3.3 用Monitor类实现互斥

也可以使用Monitor类保护共享资源不被多个线程或进程同时访问。

1)Monitor类通过向单个线程授予对象锁来控制对对象的访问。只有拥有对象锁的线程才能执行临界区的代码,此时其他任何线程都不能(再)获取该对象锁。

2)只能使用Monitor类中的静态方法,不能创建Monitor类的实例。Monitor类中的静态方法主要有:

(1)方法Enter:获取参数指定对象的对象锁。此方法放在临界区的开头。

(2)方法Wait:释放参数指定对象的对象锁,以便允许其他被阻塞的线程获取对象锁。

(3)方法Pulse和PulseAll:向等待线程队列中第一个或所有等待参数指定对象的对象锁的线程发送信息,占用对象锁的线程准备释放对象锁。

(4)方法Exit:释放参数指定对象的对象锁。

4 生产者线程和消费者线程的同步

在多线程应用中,产生数据,并把数据存到公共数据区的线程称之为生产者,使用数据,从公共数据区取出数据的线程称之为消费者。显然如果公共数据区只能存一个数据,那么在消费者线程取出数据前,生产者线程不能放新数据到公共数据区,否则消费者线程将丢失数据。如果生产者线程还没有把数据放到公共数据区,这就表示公共数据区中没有数据,那么自然消费者线程不能去取数据。

上面叙述的就是所谓的生产者和消费者关系,实现时必须要求生产者线程和消费者线程同步。

下面以一个具体的例程来讲解生产者线程和消费者线程同步的实现的具体方法:

首先定义1个全局布尔变量(用作产生/使用数据的标志变量):bool mark=false。

1)变量mark=false时,表示(1)数据还未放到公共数据区(这里假设为变量x)中。

(2)生产者线程可以放数据到公共数据区中,由于没有数据,消费线程不能取数据,必须等待。

2)变量mark=true时,表示(1)数据已放到公共数据区(x)中,消费线程还未取数据。

(2)由于公共数据区(x)中有了数据,消费线程可以取数据。(3)这时生产者线程不能再放新数据到公共数据区中,必须等待。

然后按照下面的样例,安排生产者线程和消费者线程要执行的代码,就可实现同步。

在上述例中,生产者线程和消费者线程同步是通过等待Monitor.Pulse()来完成的。生产者生产一个值,同时消费者处于等待状态,生产者的“脉冲(Pulse)”过来通知它生产完成,消费者进入消费状态,生产者等待消费者完成操作后将调用Monitor.Pulese()发出的“脉冲”。

5 结束语

多线程共享数据或共享存储结构时,可能造成执行结果的不确定性。事实上,上面这个简单的例子已经帮助我们解决了多线程应用程序中可能出现的大问题,只要领悟了解决线程间冲突的基本方法,很容易把它应用到比较复杂的程序中去。

摘要:生产者与消费者是多线程应用中一个必须解决的问题,它涉及到了线程之间的通讯的顺畅。通过对C#中多种线程方法的研究,有效地完成了它们之间的同步运行。

关键词:生产者和消费者,C#,线程同步

参考文献

[1]Akhter S.多核程序设计技术——通过软件多线程提升性能[M].北京:电子工业出版社,2007.

[2]周炎涛.Windows中的多线程编程技术和实现[J].计算技术与自动化,2002(3).

上一篇:会计监管机制下一篇:中餐摆台技能训练