线程同步:原子操作、锁、二元信号量、信号量、互斥量、临界区、读写锁、条件变量等

注:摘自《程序员的自我修养》相关章节。

 

原子操作

共享数据(全局变量或堆变量)的自增(++)操作在多线程环境下会出现错误是因为这个操作(一条c语句)被编译为汇编代码后不止一条指令,因此在执行的时候可能执行了一半就被调度系统打断,去执行别的代码。

我们把单指令的操作称为原子的(Atomic),因为无论如何,单条指令的执行是不会被打断的。为了避免出错,很多体系结构都提供了一些常用操作的原子指令,例如i386就有一条inc指令可以直接增加一个内存单元值。

在Windows里,有一套API专门进行一些原子操作(见下表),这些API称为InterlockedAPI

Windows API                       作用

InterlockedExchange     原子地交换两个值

InterlockedDecrement   原子地减少一个值

InterlockedIncrement     原子地增加一个值

InterlockedXor               原子地进行异或操作

 

使用这些函数时,Windows将保证是原子操作的,因此可以不用担心出现问题。遗憾的是,尽管原子操作指令非常方便,但是它们仅仅适用于比较简单的特定场合。在复杂的场合下,比如我们要保证一个复杂的数据结构更改的原子性,原子操作就力不从心了。这里我们就需要更加通用的手段:锁。

 

同步与锁

为了避免多个线程同时读写同一个数据(全局变量或堆变量)而产生不可预料的后果,我们需要将各个线程对同一个数据的访问同步(Synchronization)。所谓同步,即是指在一个线程访问数据未结束的时候,其他线程不得对同一个数据进行访问。如此,对数据的访问被原子化了。

同步的最常见方法是使用锁(Lock)。即每个线程在访问数据或资源之前首先试图获取(Acquire)锁,并在访问结束后释放(Release)锁。在锁已经被占用的时候试图获取锁时,线程会等待,直到锁重新可用。


二元信号量

二元信号量(Binary Semaphore)是最简单的一种锁,它只用两种状态:占用与非占用。它适合只能被唯一一个线程访问的资源。当二元信号量处于非占用状态时,第一个试图获取该二元信号量的线程会获得该锁,并将二元信号量置为占用状态,此后其他的所有试图获取该二元信号量的线程将会等待,知道该锁被释放。

 

信号量

对于允许多个线程并发访问的资源,多元信号量简称为信号量(Semaphore),它是一个很好的选择。一个初始值为N的信号量允许N个线程并发访问。

线程访问资源的时候首先获取信号量,进行如下操作:

■将信号量的值减1。

■如果信号量的值小于0,则进入等待状态,否则继续执行。

 

访问资源之后,线程释放信号量,进行如下操作:

■将信号量的值加1。

■如果信号量的值小于1,唤醒一个等待中的线程。

 

互斥量(Mutex)

二元信号量很类似,即资源仅同时允许一个线程访问,但和信号量不同的是,信号量在整个系统可以被任意线程获取并释放,也就是说,同一个信号量可以被系统中的一个线程获取之后由另一个线程释放而互斥量则要求哪个线程获取了互斥量,哪个线程就要负责释放这个锁,其他线程越俎代庖去释放互斥量是无效的。

 

临界区(Critical Section)

是比互斥量更加严格的同步手段。在术语中,把临界区的锁的获取称为进入临界区,而把锁的释放称为离开临界区。临界区和互斥量与信号量的区别在于,互斥量和信号量在系统中任何进程里都是可见的,也就是说,一个进程创建了一个互斥量或信号量,另一个进程试图去获取该锁时合法的。然而,临界区的作用范围仅限于本进程中,其他的进程无法获取该锁(类似于静态全局变量对全局变量)。除此之外,临界区具有和互斥量相同的性质。

 

读写锁(Read-Write Lock)

致力于一种更加特定的场合的同步。对于一段数据,多个线程同时读取总是没问题的,但假设操作都不是原子型,只要有任何一个线程试图对这个数据进行修改,就必须使用同步手段来避免出错。如果我们使用上述信号量、互斥量或临界区中的任何一种来进行同步,尽管可以保证程序争取,但对于读取频繁,而仅仅偶尔写入的情况,会显得非常低效。读写锁可以避免这个问题。对于同一个锁,读写锁由两种获取方式,共享的(Shared)独占的(Exclusive)。当锁处于自由的状态时,试图以任何一种方式获取锁都能成功,并将锁置于对应的状态。如果锁处于共享状态,其他线程以共享的方式获取锁仍然会成功,此时这个锁分配给了多个线程。然而,如果其他线程试图以独占的方式获取已经处于共享状态的锁,那么它必须等待锁被所有的线程释放。相应地,处于独占状态的锁将阻止任何其他线程获取该锁,不论它们试图以哪种方式获取。读写锁的行为可以总结为如下表:

 

读写锁状态           以共享方式获取                以独占方式获取

自由                      成功                                  成功

共享                      成功                                  等待

独占                      等待                                  等待

 

条件变量(Condition Variable)

作为一种同步手段,作用类似于一个栅栏。对于条件变量,线程可以有两种操作,首先线程可以等待条件变量,一个条件变量可以被多个线程等待。其次,线程可以唤醒条件变量,此时某个或所有等待此条件变量的线程都会被唤醒并继续支持。也就是说,使用条件变量可以让许多线程一起等待某个事件的发生,当事件发生时(条件变量被唤醒),所有的线程可以一起恢复执行。



  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值