技术开发 频道

WinCE多线程并发 同步安全不容忽视

  【IT168 专稿】Windows CE是微软公司推出的一个多任务的操作系统,WinCE实现多任务的方法是采用多线程和多进程机制。一般来说,每一种使用多线程、多进程的操作系统都或多或少的会存在着并发线程的安全问题。在上周我所负责的WinCE开发项目中就遇到了一点麻烦,我在程序开发中陷入了多线程的安全陷阱中,并为此一度束手无策。

  一般来说,多线程的安全性是有多种级别的,所以每个人谈论的线程安全级别其实并不相同。对于多线程并发的安全问题,有些人概念不清需求不明,结果是在代码中处处加锁,影响性能不说还特别容易莫名其妙的死锁,而另外有一些人则是对多线程并发敬而远之,结果是使系统的处理效率大大减弱。所以,在进行WinCE多线程编程时最重要的不是片面的应用多线程并发,而是要理解怎样才能保证多线程并发的安全性。本文分享我在WinCE开发中对多线程并发的同步安全问题的一些教训和经验总结。

  1.让我痛苦的多线程并发安全问题

  (1)线程资源被占用与并发假死

  WinCE系统支持多线程,这无疑是一件很好的事情。因为这能给系统带来更好的并发性能,但这同时也带来了另一个比较棘手的开发问题,那就是如何让多线程的资源应用是安全的,这在WinCE多线程并发编程中是非常重要的。例如,这次的开发项目中就让我饱受反复调试之苦。原因是经常出现多个线程对同一个资源进行并发操作,结果是出现资源被占用的状态报错,或者是系统出现并发假死使到效率低下而变慢。

  一般来说,多线程的并发安全性是指一个线程在被调用过程中,还未返回时又再次被其它线程调用,在这种情况下执行结果的可靠性。如果结果是可靠的,则称这个多线程并发安全的;如果结果是不可靠的,则称这个多线程并发是不安全的。不安全的原因是发生了线程资源被占用或并发冲突。因此,多线程并发安全性如果处理不当,轻则导致程序运行时出错或假死,严重的话将导致WinCE系统崩溃。

  (2)最常见的并发线程安全问题:死锁

  由于在调试过程中多次出现资源被占用的情况,而且跟踪资源是否被占用的过程是非常费心费力的。因此,在开发过程中我大量通过锁定一个资源来防止任何其它线程访问这个资源,以避免资源竞争。但没有想到的是,这又导致了多线程资源争夺的另一个常见问题:死锁。为了避免并发死锁,我只好逐一分析资源占用和死锁的过程,这个分析过程不但让我备受折磨和感到痛苦不堪,而且开发项目也一度陷入进退两难的困境之中。

  所谓并发死锁(Dead Lock)是指在WinCE编程中两个或两个以上的并发线程在执行过程中,如果每个线程持有某种资源而又都在等待别的线程释放它们现在保持着的资源,因占用资源而造成的一种互相等待的现象。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的线程就称为死锁线程。

  2. 多线程并发为什么会有安全性问题?

  (1)什么是多线程并发?

  在谈到多线程安全的时候,我们首先必须了解什么是多线程。WinCE是一个抢占多任务的操作系统,抢占多任务又称为调度,多线程是多任务的特殊形式。多线程是指允许在程序中并发执行多个指令流,每个指令流都称为一个线程,彼此间互相独立。多线程的目的是为了使多个线程并行工作以完成多项任务,以此来提高系统的效率。

  从并发性的角度来看,不仅可以在不同的进程之间并发执行,而且在同一个进程中的多个线程也是可以并发执行的。一般来说,除了一些必不可少的资源外,线程本身并不占有系统的资源。但是线程却可以和同属于一个进程的另一个线程共享进程所拥有的全部资源,而且一个线程还可以创建或者撤销另一个线程,因此当处理不当时就会发生资源冲突问题了。

  (2)并发线程的优先级问题

  WinCE操作系统具有实时性,所以调度系统必须保证高优先级线程先运行,低优先级线程只有在高优先级线程终止后或者阻塞时才能得到CPU时间片。WinCE不像其它Windows操作系统将进程分为不同的优先级类,WinCE只将线程分为256个优先级。0优先级最高,255最低。0到248优先级属于实时性优先级,一般分配给实时性应用程序、驱动程序、系统程序。248到 255优先级一般分配给普通应用程序线程使用。

  每一个线程都有五种状态,分别为运行、挂起、睡眠、阻塞、终止。一旦发生中断,WinCE内核会暂停低优先级线程的运行,让高优先级线程继续运行,直到终止或者阻塞。具有相同优先级的线程平均占有CPU时间片,当一个线程使用完了 CPU时间片或在时间片内阻塞、睡眠,其它相同优先级的线程才会占有时间片。这里提到的CPU时间片是指内核限制线程占有CPU的时间,默认为100ms。在调度过程中,内核的调度系统包含一个当前所有进程中线程的优先级列表,并对所有的线程按优先级排列顺序。这个并发调度算法表面上看起来已经很有效、很完美了,但在实际的WinCE编程中,许多开发人员却会因为经验不足而经常发生资源被占用或资源死锁。

  (3)并发安全问题起源于同步与争夺

  在WinCE多线程共享资源中防止访问冲突是极为重要的。正常来说,被允许执行的线程首先会拥有对资源的排他性访问权。当第一个线程访问资源时,第一个线程会给该访问资源加锁,而这个锁会导致其它也想访问同一资源的线程被阻塞,直至第一个线程释放它加在资源上的锁。也就是说,在WinCE系统中如果一个线程不能得到需要的某个资源,它将挂起执行(阻塞),直到该资源有效为止。所以,在WinCE系统运行过程中,各线程都将会对资源进行锁定或解锁。

  从理论上说,对资源的锁定或解锁动作应该是能避免资源竞争情况的出现。但由于各线程运行和指向其资源的相对时序各不相同,就有可能出现由于各个线程正在等待被其它线程保持的资源,从而导致所有线程都无法运行的情况。如果系统中所有进程都进入死锁,即没有一个进程能前进的话,称为系统死锁,这种情况是很少见的,我们在这次项目中经常遇到的是进程死锁。

  3.应用同步机制,避免并发安全问题发生

  WinCE系统的线程要经常在某些条件下进行状态切换,这可能是自身行为引起的,也可能是外界行为引起。在大多数情况下,这些线程之间是要相互通信、相互协调才能完成任务的。WinCE给我们提供了很多的同步机制,包括临界区、互斥体、信号量、事件、互锁函数和消息队列等。这里只介绍我在这次项目中应用的几种主要方法:

  (1)临界区

  临界区(Critical Section)是一种防止多个并发线程同时执行一个特定代码节的机制。在这次项目反复的调试中,我得到一个宝贵的经验,就是在跟踪代码中的多线程处理性能时,对 WinCE中的临界区深刻理解是非常有用的。临界区是一种轻量级机制,它在某一时间内只只允许一个线程执行某个给定的代码段。因此,通过对临界区进行有效管理,使得某一时刻最多只能有一个线程进入临界区,就能实现对临界资源的保护。

  临界区对象是运行在用户模式的,利用临界区可以实现对临界资源的互斥操作。它能保证在临界区内所有被访问的资源不被其它线程访问,直到当前线程执行完临界区代码。我在这次项目中得到的教训是:WinCE的临界区是只能应用在同一进程内,亦即实现的是同一进程内的线程间同步,不能应用在进程之间,这一点的实践经验可谓是血汗的结晶。例如,当有两个线程,每个线程都有临界区,而且临界区保护的资源有相同的时候,这时就要在编写代码时多加考虑。另外,在使用临界区时一定要注意避免死锁,如果能够更深入地了解临界区的工作原理,则这一情形的出现就不会令人太沮丧。

  (2)互斥体

  互斥体(Mutex)顾名思义就是实现对共享资源实现互斥的访问。在使用上,互斥体与临界区都能实现的对共享资源的互斥访问,但是临界区是多个线程对同一段程序的执行,这段程序会访问到临界资源,所以它们是同一个进程内的多个线程;而互斥体的应用情景则是在线程之间的独立执行,可以不是程序上的重叠,只是一个线程执行到共享资源的时候,有可能别的线程也要访问该共享资源,所以要用互斥体来保护该共享资源。

  一般来说,互斥对象是运行在内核模式的,它的行为特性同临界区非常相似,在一个线程访问某个共享资源时,它能够保证其它线程不能访问这个资源。不同的是互斥对象是运行在内核模式,从时间上比临界区要慢。但由于内核对象具有全局性,不同的进程都能够访问,这样利用互斥对象就可以让不同的进程中的线程互斥访问一个共享资源,而临界区只能在一个进程内有效,这一点在使用上需要特别的注意,否则容易误用导致经常报错。

  (3)其它的并发同步机制和方法

  WinCE系统中还广泛使用事件(Event)机制来实现线程之间的协调工作。具体方式是用通知来告知一个线程什么时候去执行它的特定的任务,并标识事件的发生。而信号量(Semaphore)同步机制是用一个数值表示当前该信号量的使用情况,当前值的大小处于零和最大值之间。在WinCE中如果把信号量的最大值和初始值均设置为1,那么它就可实现互斥体,即保证对共享资源互斥访问的保护。如果把信号量的初始值设置为0,就是要等待别的线程来唤醒它,那么它就可实现事件机制。信号量机制的最大特点是可以用在同一进程内的线程之间同步,也可以用在进程之间的同步。

  消息队列通信(MsgQueue P2P)同步机制是如同建立了一个管道,各方的线程分别在管道的两端建立读/写端口。管道内的消息组成一个先进先出FIFO(First In First Out)的队列,在读端口读取的消息是读取管道内消息组队列的头消息,写入消息时则是在写端口的队列尾部追加一个消息。消息队列既可以作为在线程之间传递数据的工具,也可以作为线程之间同步的工具。

  除了上面的同步方法之外,WinCE还提供了一些用于原子操作的互锁函数。这些函数在执行过程中,不会因为线程的调度引起的当前线程被抢占而打断函数内的操作。互锁函数运行在用户模式,它能保证当一个线程访问一个变量时,其它线程无法访问此变量,以确保变量值的唯一性。这种访问方式也被称为原子访问。总的来说,WinCE多线程编程是一个很重要的功能,但必须要正确的使用同步机制来保证其安全性。

0
相关文章