部分内容转自:http://blog.youkuaiyun.com/zouxinfox/article/details/5838861
http://blog.youkuaiyun.com/goodluckwhh/article/details/8564319
http://blog.youkuaiyun.com/anonymalias/article/details/9219945
概述
同步和互斥在多线程和多进程编程中是一个基本的需求,互相协作的多个进程和线程往往需要某种方式的同步和互斥。POSIX定义了一系列同步对象用于同步和互斥。
同步对象是内存中的变量属于进程中的资源,可以按照与访问数据完全相同的方式对其进行访问。默认情况下POSIX定义的这些同步对象具有进程可见性,即同步对象只对定义它的进程可见;但是通过修改同步对象的属性可以使得同步对象对不同的进程可见,具体的做法是:
- 修改同步对象的属性为PTHREAD_PROCESS_SHARED
- 在进程的特殊内存区域--共享内存中创建同步对象
其中设置共享对象的属性为 PTHREAD_PROCESS_SHARED是为了告诉系统该共享对象是跨越进程的 ,不仅仅对创建它的进程可见;但是仅有这一个条件显然无法满足不同进程使用该同步对象的需求,因为每个进程的地址空间是独立的, 位于一个进程的普通内存区域中的对象是无法被其它进程所访问的,能满足这一要求的内存区域是共享内存,因而 同步对象要在进程的共享内存区域内创建 。
同步对象还可以放在文件中。同步对象可以比创建它的进程具有更长的生命周期。
最常见的进程/线程的同步方法有互斥锁(或称互斥量Mutex),读写锁(rdlock),条件变量(cond),信号量(Semophore)等。在Windows系统中,临界区(Critical Section)和事件对象(Event)也是常用的同步方法。
1.互斥锁--保护了一个临界区,在这个临界区中,一次最多只能进入一个线程。如果有多个进程在同一个临界区内活动,就有可能产生竞态条件(race condition)导致错误,其中包含递归锁和非递归锁,(递归锁:同一个线程可以多次获得该锁,别的线程必须等该线程释放所有次数的锁才可以获得)。
2.读写锁--从广义的逻辑上讲,也可以认为是一种共享版的互斥锁。可以多个线程同时进行读,但是写操作必须单独进行,不可多写和边读边写。如果对一个临界区大部分是读操作而只有少量的写操作,读写锁在一定程度上能够降低线程互斥产生的代价。
3.条件变量--允许线程以一种无竞争的方式等待某个条件的发生。当该条件没有发生时,线程会一直处于休眠状态。当被其它线程通知条件已经发生时,线程才会被唤醒从而继续向下执行。条件变量是比较底层的同步原语,直接使用的情况不多,往往用于实现高层之间的线程同步。使用条件变量的一个经典的例子就是线程池(Thread Pool)了。
4.信号量--通过精心设计信号量的PV操作,可以实现很复杂的进程同步情况(例如经典的哲学家就餐问题和理发店问题)。而现实的程序设计中,却极少有人使用信号量。能用信号量解决的问题似乎总能用其它更清晰更简洁的设计手段去代替信号量。
5.自旋锁--当要获取一把自旋锁的时候又被别的线程持有时,不断循环的去检索是否可以获得自旋锁,一直占CPU资源。
对于这些同步对象,有一些共同点:
- 每种类型的同步对象都有一个init的API,它完成该对象的初始化,在初始化过程中会分配该同步对象所需要的资源(注意是为支持这种锁而需要的资源,不包括表示同步对象的变量本身所需要的内存)
- 每种类型的同步对象都一个destory的API,它完成与init相反的工作
- 对于使用动态分配内存的同步对象,在使用它之前必须先调用init
- 在释放使用动态分配内存的同步对象所使用的内存时,必须先调用destory释放系统为其申请的资源
- 每种同步对象的默认作用范围都是进程内部的线程,但是可以通过修改其属性为PTHREAD_PROCESS_SHARED并在进程共享内存中创建它的方式使其作用范围跨越进程范围
- 无论是作用于进程内的线程,还是作用于不同进程间的线程,真正参与竞争的都是线程(对于不存在多个线程的进程来说就是其主线程),因而讨论都基于线程来
- 这些同步对象都是协作性质的,相当于一种君子协定,需要相关线程主动去使用,无法强制一个线程必须使用某个同步对象
总体上来说,可以将它们分为两类:
- 第一类是互斥锁、读写锁、自旋锁,它们主要是用来保护临界区的,也就是主要用于解决互斥问题的,当尝试上锁时大体上有两种情况下会返回:上锁成功或出错,它们不会因为出现信号而返回。另外解锁只能由锁的拥有着进行
- 第二类是条件变量和信号量,它们提供了异步通知的能力,因而可以用于同步和互斥。但是二者又有区别:
- 信号量可以由发起P操作的线程发起V操作,也可以由其它线程发起V操作;但是条件变量一般要由其它线程发起signal(即唤醒)操作
- 由于条件变量并没有包含任何需要检测的条件的信息,因而对这个条件需要用其它方式来保护,所以条件变量需要和互斥锁一起使用,而信号量本身就包含了相关的条件信息(一般是资源可用量),因而不需要和其它方式一起来使用
- 类似于三种锁,信号量的P操作要么成功返回,要么失败返回,不会因而出现信号而返回;但是条件变量可能因为出现信号而返回,这也是因为它没包含相关的条件信息而导致的。
文章会涉及到递归锁与非递归锁(recursive mutex和non-recursive mutex),区域锁(Scoped Lock),策略锁(Strategized Locking),读写锁与条件变量,双重检测锁(DCL),锁无关的数据结构(Locking free),自旋锁等等内容
一、互斥锁
互斥锁(mutex):顾名思义即是相互排斥的锁,它是最基本的同步形式,用于保护临界区,以保证任何时刻都只有一个线程在执行临界区中的代码。在任意时刻都只有一个线程能持有这把锁,当已经有线程拿到这把锁的时候,其它请求获得这把锁的线程将会被阻塞直到持有锁的线程释放了这把锁或者得到一个EBUSY的错误。
最常见互斥场景是:多个线程会访问它们共享的资源,为了保证资源的一致性,因而需要进行互斥访问,以保证任何时刻都只有一个线程在访问该资源。因而大部分场景下临界区中放的都是操作互斥资源的代码。
互斥锁解决同步问题就不是那么合适了,因为需要同步的线程之间往往有依赖或者顺序关系,但是互斥锁自己无法保证这个顺序。
临界区的基本形式为:
mutex_lock(...);
临界区即位于lock之后,unlock之前的部分是临界区。
mutex_unlock(...);
1、创建和销毁
创建和销毁
- pshared: PTHREAD_PROCESS_PRIVATE
- type: PTHREAD_MUTEX_DEFAULT
- protocol: PTHREAD_PRIO_NONE
- prioceiling: –
- robustness: PTHREAD_MUTEX_STALLED_NP
2、属性对象
3、类型属性
4、锁的操作
5、其他
当前线程已经拥有互斥锁,如果定义了 _POSIX_THREAD_PRIO_INHERIT 符号,则会使用协议属性值 PTHREAD_PRIO_INHERIT 对互斥锁进行初始化。属主失败时的行为取决于pthread_mutexattr_setrobust_np()的 robustness 参数的值。
- 创建和该条件相关联的条件变量,并初始化它
- 对于线程A来说,它需要做的是设置这个条件,通知等待在相关联条件变量上的线程
- 对于线程B来说,它需要做的是检查这个条件,如果不满足自己的要求,就阻塞在相关联的条件变量上
- 一个线程调用 pthread_cond_signal或 pthread_cond_broadcast
- 另一个线程已经测试了该条件,但是尚未调用 pthread_cond_wait
- 没有正在等待的线程,因而pthread_cond_signal或 pthread_cond_broadcast的唤醒将无法起作用,该唤醒会被丢失
对比下信号,信号可以做到通知其它线程某件事发生了,接收信号的线程只需要注册一个信号处理函数,然后信号发生后该处理函数就会被系统调用,一旦该函数被调用了就意味着注册时关联的信号所代表的事情发生了。但要注意:
- POSXI要求多线程应用中信号处理程序必须在应用的多个线程之间共享(即在一个进程的多个线程之间共享),因而对于同一个进程中的多个线程来说它们必须共享信号处理程序,信号处理程序无法确定信号是被发给谁的
- 使用信号时只需要注册信号处理程序即可,不需要创建某种同步对象,而使用条件变量需要创建同步对象,如果要在进程间进行同步和互斥还对条件变量的作用域和属性有要求
- 有权限的任何用户的任何程序都可以发送信号给一个线程,而使用条件变量时,相关的线程必须可以访问同步对
设有两个共享的变量 x 和 y,通过互斥量 mut 保护,当 x > y 时,条件变量 cond 被触发。先执行func1,之后再执行func2,
int x = 0,y = 5;
pthread_mutex_t mut = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
void *func1(void *arg)
while (x <= y) {
pthread_cond_wait(&cond, &mut);
}
/* 对 x、y 进行操作 */
pthread_mutex_unlock(&mut);
}
void *func2(void *arg)
pthread_mutex_lock(&mut);
x = 5;
if (x > y)
pthread_mutex_unlock(&mut);
如果条件变量是动态分配的,则必须在使用它之前用pthread_cond_init来初始化它。pthread_cond_init用来初始化cv所指向的条件变量,如果cattr为NULL则会用缺省的属性初始化条件变量;否则使用cattr指定的属性初始化条件变量。使用PTHREAD_COND_INITIALIZER 宏与动态分配具有null 属性的 pthread_cond_init()等效,不同之处在于PTHREAD_COND_INITIALIZER 宏不进行错误检查。
多个线程决不能同时初始化或重新初始化同一个条件变量。如果要重新初始化或销毁某个条件变量,则应用程序必须确保该条件变量未被使用。
- 由pthread_cond_signal唤醒
- 由pthread_cond_broadcast唤醒
- 由信号唤醒
pthread_cond_wait在被唤醒之前将一致保持阻塞状态。它会在被阻塞之前以原子方式释放相关的互斥锁,并在返回之前以原子方式再次获取该互斥锁 。
通常情况下对条件表达式的检查是在互斥锁的保护下进行的。如果条件表达式为假,线程就会基于条件变量阻塞。然后,当其它线程更改条件值时,就会唤醒它(通过pthread_cond_signal或pthread_cond_broadcast)。 这种变化会导致至少一个正在等待该条件的线程解除阻塞并尝试再次获取互斥锁。
必须重新测试导致等待的条件,然后才能从 pthread_cond_wait处继续执行 。唤醒的线程重新获取互斥锁并从pthread_cond_wait返回之前,条件可能会发生变化。等待线程锁等待的条件可能并未真正发生。通常使用条件变量的方式如下:
pthread_cond_wait是一个取消点。如果有一个未决的取消请求并且该线程启用了取消功能,则该线程会被终止并在继续持有锁的状态下开始执行的清理处理函数。如果清理处理函数中未释放锁,则就会出现线程终止但是未释放锁的情形。
应在互斥锁的保护下修改相关条件,该互斥锁应该是与该条件变量相关联的那个互斥锁(即调用wati时指定的那个互斥锁)。否则,可能在条件变量的测试和pthread_cond_wait阻塞之间修改该变量,这会导致无限期等待。
如果有多个线程在等待一个条件变量,则线程被唤醒的顺序由所采用的调度策略决定。
- 如果使用的是默认的调度策略,即SCHED_OTHER,则无法保证被唤醒的顺序
- 如果使用的是SCHED_FIFO 或SCHED_RR,则线程按照优先级被唤醒
pthread_cond_reltimedwait_np与pthread_cond_timedwait基本相同,它们唯一的区别在于pthread_cond_reltimedwait_np使用相对时间间隔而不是将来的绝对时间作为其最后一个参数的值。
类似于pthread_cond_wait,pthread_cond_reltimedwait_np和pthread_cond_timedwait也是取消点。
由于pthread_cond_broadcast会导致所有基于该条件阻塞的线程再次争用互斥锁,因此即便使用了pthread_cond_broadcast实际上最终也只有一个线程可以获得锁并开始运行。虽然都是只有一个线程可以运行,但是这种情形与pthread_cond_signal是有所区别的:
- 如果有多个线程阻塞在条件变量上,并且pthread_cond_signal唤醒了其中一个线程,则其它线程仍然在等待被唤醒然后再尝试获取相应的互斥锁,它们阻塞在条件变量上
- 如果有多个线程阻塞在条件变量上,并且pthread_cond_broadcast唤醒它们,则所有线程都开始竞争互斥锁,胜利者开始执行,失败者阻塞在互斥锁上
当线程A想要获取一把自选锁而该锁又被其它线程锁持有时,线程A会在一个循环中自选以检测锁是不是已经可用了。对于自选锁需要注意:
- 由于自旋时不释放CPU,因而持有自旋锁的线程应该尽快释放自旋锁,否则等待该自旋锁的线程会一直在那里自旋,这就会浪费CPU时间。
- 持有自旋锁的线程在sleep之前应该释放自旋锁以便其它线程可以获得自旋锁。(在内核编程中,如果持有自旋锁的代码sleep了就可能导致整个系统挂起,最近刚解决了一个内核中的问题就是由于持有自旋锁时sleep了,然后导致所有的核全部挂起(是一个8核的CPU))
- 建立锁所需要的资源
- 当线程被阻塞时锁所需要的资源
对于 互斥锁 来说,与自旋锁相比它需要 消耗大量的系统资源来建立锁 ;随后当线程被阻塞时,线程的调度状态被修改,并且线程被加入等待线程队列;最后当锁可用时,在获取锁之前,线程会被从等待队列取出并更改其调度状态;但是在线程被阻塞期间,它不消耗CPU资源。
因此自旋锁和互斥锁适用于不同的场景。自旋锁适用于那些仅需要阻塞很短时间的场景,而互斥锁适用于那些可能会阻塞很长时间的场景。
- PTHREAD_PROCESS_SHARED:该自旋锁可以在多个进程中的线程之间共享。
- PTHREAD_PROCESS_PRIVATE:仅初始化本自旋锁的线程所在的进程内的线程才能够使用该自旋锁。