参考
glibc nptl库pthread_mutex_lock和pthread_mutex_unlock浅析
概念
互斥锁和条件变量出自posix.1线程标准,它们总是可用来同步一个进程内的各个线程的,如果一个互斥锁或条件变量存放在多个进程间共享的某个内存区内,那么posix还允许它用于这些个进程间的同步。
Posix mutex
互斥锁(英语:Mutual exclusion,缩写 Mutex)是一种用于多线程编程中,防止两条线程同时对同一公共资源(比如全局变量)进行读写的机制。该目的通过将代码切片成一个一个的临界区域(critical section)达成。临界区域指的是一块对公共资源进行访问的代码,并非一种机制或是算法。一个程序、进程、线程可以拥有多个临界区域,但是并不一定会应用互斥锁。
Posix Thread中的条件变量
pthread中,条件变量实际上是一个阻塞线程处于睡眠状态的线程队列。每个条件变量都必须与一个(且建议只能是一个)互斥锁配套使用。一个线程首先获得互斥锁,然后检查或者修改“条件”;如果条件不成立,则调用pthread_cond_wait(),依次实施3个操作:
1 对当前互斥锁解锁(以便其它线程可以访问或者修改“条件”)
2 把线程自身阻塞挂起到互斥锁的线程队列中
3 被其它线程的信号从互斥锁的线程队列唤醒后,对当前配套的互斥锁申请加锁,如果加锁未能成功,则阻塞挂起到当前互斥锁上。pthread_cond_wait() 函数将不返回直到线程获得配套的互斥锁。
线程离开“条件”的临界区时,必须对当前互斥锁执行解锁。
NPTL
Native POSIX Thread Library(NPTL)是Linux内核中实践POSIX Threads标准的库
在Linux内核2.6出现之前进程是(最小)可调度的对象,当时的Linux不真正支持线程。但是Linux内核有一个系统调用指令clone()
,这个指令产生一个调用调用的进程的复件,而且这个复件与原进程使用同一地址空间。LinuxThreads计划使用这个系统调用来提供一个内核级的线程支持。但是这个解决方法与真正的POSIX标准有一些不兼容的地方,尤其是在信号处理、进程调度和进程间同步原语方面。
要提高LinuxThreads的效应很明显需要提供内核支持以及必须重写线程函数库。为了解决这个问题出现了两个互相竞争的项目:一个IBM的组的项目叫做NGPT(Next Generation POSIX Threads,下一代POSIX线程),另一个组是由Red Hat程序员组成的。2003年中NGPT被放弃,几乎与此同时NPTL公布了。
NPTL首次是随Red Hat Linux 9发表的。此前老式的Linux POSIX线程偶尔会发生系统无法产生线程的毛病,这个毛病的原因是因为在新线程开始的时候系统没有借机先占。当时的Windows系统对这个问题的解决比较好。Red Hat在关于Red Hat Linux 9上的Java的网页上发表了一篇文章称NPTL解决了这个问题[3]。
从第3版开始NPTL是Red Hat Enterprise Linux的一部分,从Linux内核2.6开始它被纳入内核。当前它完全被结合入GNU C
库。
NPTL的解决方法与LinuxThreads类似,内核看到的首要抽象依然是一个进程,新线程是通过clone()系统调用产生的。但是NPTL需要特殊的内核支持来解决同步的原始类型之间互相竞争的状况。在这种情况下线程必须能够入眠和再复苏。用来完成这个任务的原始类型叫做futex
。
NPTL是一个所谓的1×1线程函数库
。用户产生的线程与内核能够分配的对象之间的联系是一对一的。这是所有线程程序中最简单的。
互斥锁
GNU C Library (GNU libc) stable release version 2.17, by Roland McGrath et al.
pthread_mutex_t
1 | /* Data structures for mutex handling. The structure of the attribute |
PTHREAD_MUTEX_INITIALIZER
静态初始化的宏
1 | static pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER; |
pthread_mutex_init
动态初始化的函数(malloc或者分配在共享内存区)
1 | int |
比较复杂,不研究了
unp:
你可能会碰到省略了初始化操作的代码,因为它所在的实现把初始化常量定义为0(而且静态分配的变量被自动得初始化为0),不过这是不正确的代码
__pthread_mutex_lock
1 | //pthread_mutex_lock.c |
- 如果是普通锁,调用LLL_MUTEX_LOCK,进行CAS(Compare-and-Swap)操作,失败则执行系统调用sys_futex陷入内核态
- 如果是递归锁,判断是否是当前进程(线程)持有锁。然后将count计数器加一,如果count等于0,则return EAGAIN
- 如果是自适应锁,通过非阻塞的LLL_MUTEX_TRYLOCK自旋到最大次数后,执行LLL_MUTEX_LOCK
- 如果是检错锁,判断是否是当前进程(线程)重复加锁,返回EDEADLK
__pthread_mutex_unlock
1 | int |
- 如果是普通锁,执行lll_unlock
- 如果是递归锁,判断是否是当前的进程(线程)持有锁,如果是,count计数器减一,count为0的话,返回
- 如果是自适应锁,执行lll_unlock
- 如果是检错锁,判断是否是当前的进程(线程)持有锁,如果是,执行lll_unlock。如果不是,返回EPERM
__pthread_mutex_trylock
1 | int |
非阻塞获取锁,具体不分析了
__pthread_mutex_destroy
1 | int |
摧毁互斥锁
四种锁的属性
互斥锁的属性在创建锁的时候指定,在LinuxThreads实现中仅有一个锁类型属性,不同的锁类型在试图对一个已经被锁定的互斥锁加锁时表现不同。当前(glibc2.2.3,linuxthreads0.9)有四个值可供选择:
PTHREAD_MUTEX_TIMED_NP,这是缺省值,也就是普通锁。当一个线程加锁以后,其余请求锁的线程将形成一个等待队列,并在解锁后按优先级获得锁。这种锁策略保证了资源分配的公平性
PTHREAD_MUTEX_RECURSIVE_NP,嵌套锁,允许同一个线程对同一个锁成功获得多次,并通过多次unlock解锁。如果是不同线程请求,则在加锁线程解锁时重新竞争。
PTHREAD_MUTEX_ADAPTIVE_NP 适应锁,动作最简单的锁类型,仅等待解锁后重新竞争。线程旋转直到达到最大旋转计数或获得锁定为止https://stackoverflow.com/questions/19863734/what-is-pthread-mutex-adaptive-np
PTHREAD_MUTEX_ERRORCHECK_NP 检错锁,如果同一个线程请求同一个锁,则返回EDEADLK,否则与PTHREAD_MUTEX_TIMED_NP类型动作相同。这样就保证当不允许多次加锁时不会出现最简单情况下的死锁。
条件变量
pthread_cond_t
1 | /* Data structure for conditional variable handling. The structure of |
PTHREAD_COND_INITIALIZER
1 | /* Conditional variable handling. */ |
静态初始化
__pthread_cond_init
1 | int |
动态初始化
__pthread_cond_wait
1 | int |
无条件等待pthread_cond_wait(),必须和一个互斥锁配合,以防止多个线程同时请求pthread_cond_wait()(或pthread_cond_timedwait(),下同)的竞争条件(Race Condition)。mutex互斥锁必须是普通锁(PTHREAD_MUTEX_TIMED_NP)或者适应锁(PTHREAD_MUTEX_ADAPTIVE_NP),且在调用pthread_cond_wait()前必须由本线程加锁(pthread_mutex_lock()),而在更新条件等待队列以前,mutex保持锁定状态,并在线程挂起进入等待前解锁。在条件满足从而离开pthread_cond_wait()之前,mutex将被重新加锁,以与进入pthread_cond_wait()前的加锁动作对应。
激发条件有两种形式,pthread_cond_signal()激活一个等待该条件的线程,存在多个等待线程时按入队顺序激活其中一个;而pthread_cond_broadcast()则激活所有等待线程。
__pthread_cond_timedwait
1 | int |
超时等待__pthread_cond_timedwait(),必须和一个互斥锁配合,以防止多个线程同时请求pthread_cond_wait()(或pthread_cond_timedwait(),下同)的竞争条件(Race Condition)。mutex互斥锁必须是普通锁(PTHREAD_MUTEX_TIMED_NP)或者适应锁(PTHREAD_MUTEX_ADAPTIVE_NP),且在调用pthread_cond_wait()前必须由本线程加锁(pthread_mutex_lock()),而在更新条件等待队列以前,mutex保持锁定状态,并在线程挂起进入等待前解锁。在条件满足从而离开pthread_cond_wait()之前,mutex将被重新加锁,以与进入pthread_cond_wait()前的加锁动作对应。
激发条件有两种形式,pthread_cond_signal()激活一个等待该条件的线程,存在多个等待线程时按入队顺序激活其中一个;而pthread_cond_broadcast()则激活所有等待线程。
__pthread_cond_signal
1 | int |
唤醒单个线程lll_futex_wake (&cond->__data.__futex, 1, pshared);
__pthread_cond_broadcast
1 | int |
唤醒所有等待的线程lll_futex_wake (&cond->__data.__futex, INT_MAX, pshared);
pthread_cond_destroy
1 | int |
在释放或废弃条件变量之前,需要毁坏它
总结
- pthread_cleanup_push和pthread_cleanup_pop作为线程取消的回调函数,在wait函数中执行,防止线程退出导致的锁未释放,进而出现死锁的情况
- 条件变量机制不是异步信号安全的,也就是说,在信号处理函数中调用pthread_cond_signal()或者pthread_cond_broadcast()很可能引起死锁。
- wait函数先解锁,然后加入睡眠列表,没有忙轮询的消耗,被其他线程”唤醒”后重新加锁
- wait函数执行前当前线程必须已经对mutex加锁,并且锁的类型是普通锁或者适应锁