例如pthread(posix)锁原语,它们是可重入的还是不可重入的?在使用它们时应避免哪些陷阱?
互斥锁是可重入的吗?
可重入锁
可重入锁是一种进程可以多次申请锁而不会因此被阻塞的锁。它在难以跟踪是否已经获得锁的情况下非常有用。如果锁是不可重入的,你可能会先获得锁,然后在再次尝试获取锁时被阻塞,从而导致自己的进程陷入死锁。
可重入性通常是代码没有中央可变状态的特性,如果在执行代码时调用了该代码,就不会破坏任何东西。这样的调用可以由另一个线程进行,也可以由代码本身发起的执行路径递归进行。
如果代码依赖于可能在执行过程中更新的共享状态,则它不是可重入的,至少如果该更新会破坏它的话。
可重入锁的使用场景
可重入锁的一个(有些泛化和牵强附会)示例应用程序可能是:
您有一些计算涉及遍历图形的算法(可能具有其中的循环)。遍历可能会因为循环或多条到达同一节点的路径而多次访问同一节点。
数据结构会受到并发访问,并且由于某些原因可能会进行更新,例如由另一个线程。您需要能够锁定单个节点以处理由于竞态条件引起的潜在数据损坏。出于某种原因(例如性能),您不想全局锁定整个数据结构。
您的计算无法保留有关已访问的节点的完整信息,或者正在使用不允许快速回答“我之前是否来过”问题的数据结构。
在这种情况下,一个简单的实现Dijkstra算法的例子是,使用二叉堆作为优先级队列,或使用简单的链表作为队列的广度优先搜索。在这些情况下,扫描队列以查找现有插入是O(N)的,您可能不想在每次迭代时执行它。
在这种情况下,跟踪已经获取的锁是昂贵的。假设您想在节点级别执行锁定,可重入锁定机制可以减轻需要告知是否访问过节点的需求。您只需盲目地锁定节点,可能在将其从队列中弹出后解锁。
可重入互斥锁
简单的互斥锁不是可重入的,因为在给定时间内只能有一个线程处于临界区域内。如果您抓住了互斥锁,然后尝试再次抓住它,则简单的互斥锁没有足够的信息告诉您之前谁持有它。要递归地执行此操作,需要一种机制,其中每个线程都有一个标记,以便您可以知道谁抓住了互斥锁。这使互斥锁机制变得更加昂贵,因此您可能不想在所有情况下都这样做。
我记得 POSIX 线程 API 提供了可重入和非可重入互斥锁选项。
可重入锁允许你编写一个方法M
,它能够对资源A
进行加锁,并且可以递归地调用M
或从已经持有A
锁的代码中调用M
。
使用不可重入锁,你需要编写两个版本的M
,一个进行加锁,另一个不进行加锁,并且还需要额外的逻辑来调用正确的版本。
x
次,我不能在不释放所有递归获取的锁(相同的锁但是获取了 x
次)的情况下交错执行?如果是这样,那么它本质上使得这个实现成为顺序执行。我有什么遗漏吗? - DevdattaK#include <pthread.h>
#include <stdio.h>
pthread_mutex_t lock;
void * func1(void *arg){
printf("thread1\n");
pthread_mutex_lock(&lock);
printf("thread1 hey hey\n");
}
void * func2(void *arg){
printf("thread2\n");
pthread_mutex_lock(&lock);
printf("thread2 hey hey\n");
}
int main(){
pthread_mutexattr_t lock_attr;
int error;
// error = pthread_mutexattr_settype(&lock_attr, PTHREAD_MUTEX_RECURSIVE);
error = pthread_mutexattr_settype(&lock_attr, PTHREAD_MUTEX_DEFAULT);
if(error){
perror(NULL);
}
pthread_mutex_init(&lock, &lock_attr);
pthread_t t1, t2;
pthread_create(&t1, NULL, func1, NULL);
pthread_create(&t2, NULL, func2, NULL);
pthread_join(t2, NULL);
}
输出:
thread1
thread1 hey hey
thread2
常见死锁示例,没有问题。
只需取消此行注释:
error = pthread_mutexattr_settype(&lock_attr, PTHREAD_MUTEX_RECURSIVE);
并注释掉另一行。
输出:
thread1
thread1 hey hey
thread2
是的,递归互斥锁也可能导致死锁。
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
pthread_mutex_t lock;
void func3(){
printf("func3\n");
pthread_mutex_lock(&lock);
printf("func3 hey hey\n");
}
void * func1(void *arg){
printf("thread1\n");
pthread_mutex_lock(&lock);
func3();
printf("thread1 hey hey\n");
}
void * func2(void *arg){
printf("thread2\n");
pthread_mutex_lock(&lock);
printf("thread2 hey hey\n");
}
int main(){
pthread_mutexattr_t lock_attr;
int error;
// error = pthread_mutexattr_settype(&lock_attr, PTHREAD_MUTEX_RECURSIVE);
error = pthread_mutexattr_settype(&lock_attr, PTHREAD_MUTEX_DEFAULT);
if(error){
perror(NULL);
}
pthread_mutex_init(&lock, &lock_attr);
pthread_t t1, t2;
pthread_create(&t1, NULL, func1, NULL);
sleep(2);
pthread_create(&t2, NULL, func2, NULL);
pthread_join(t2, NULL);
}
输出:
thread1
func3
thread2
thread t1
在func3
中发生了死锁。
(我使用sleep(2)
使得可以更容易地看出死锁首先是由于在func3
中重新加锁导致的)
同样地,取消注释递归锁行并注释掉另一行。
输出:
thread1
func3
func3 hey hey
thread1 hey hey
thread2
thread t2
中出现了死锁,在func2
中。明白吗?func3
完成并退出,重新锁定不会阻塞线程或导致死锁。
那么,最后一个问题,我们为什么需要它?
对于递归函数(在多线程程序中调用并想要保护某些资源/数据),就需要使用它。
例如,你有一个多线程程序,在线程A中调用一个递归函数。你有一些数据需要在该递归函数中进行保护,因此你使用互斥机制。在线程A中,该函数的执行是顺序的,因此在递归中肯定会重新锁定互斥量。使用普通的互斥量会导致死锁,而可重入互斥量被发明来解决这个问题。
请参见接受答案中的示例:When to use recursive mutex?
维基百科非常好地解释了可重入互斥量。值得一读。Wikipedia: Reentrant_mutex