一般来说,什么是可重入锁和概念?

我总是很困惑。有人能解释一下 再入者在不同的语境中是什么意思吗?为什么要使用可重入和不可重入?

比如说 pthread (posx)锁原语,它们是否是可重入的? 使用它们时应该避免哪些陷阱?

互斥对象是可重入的吗?

64987 次浏览

可重入锁定

可重入锁是一种进程可以多次声明锁而不会对自身造成阻塞的锁。在不容易记录你是否已经拿到了一把锁的情况下,它是非常有用的。如果一个锁是不可重入的,那么您可以抓取这个锁,然后在再次抓取它的时候阻塞它,从而有效地使您自己的进程死锁。

可重入性通常是代码的一个属性,在这个属性中,如果在执行代码时调用该代码,则没有可损坏的中央可变状态。这样的调用可以由另一个线程发出,也可以由源自代码本身的执行路径递归发出。

如果代码依赖于可以在执行过程中更新的共享状态,那么它就不是可重入的,至少在该更新可能破坏它的情况下不是这样。

可重入锁定的用例

可重入锁应用程序的一个(有些通用和人为的)示例可能是:

  • 您有一些计算涉及一个遍历图的算法(可能在其中包含循环)。由于循环或同一节点的多个路径,遍历可能多次访问同一节点。

  • 数据结构受到并发访问的影响,并且由于某种原因可能由另一个线程进行更新。您需要能够锁定单个节点,以处理由于竞态条件而可能出现的数据损坏。出于某种原因(可能是性能) ,您不希望全局锁定整个数据结构。

  • 您的计算无法保留关于所访问的节点的完整信息,或者您使用的数据结构不允许快速回答“我以前来过这里吗”的问题。这种情况的一个例子是 Dijkstra 算法的一个简单实现,优先级队列实现为二进制堆,或者一个广度优先搜索使用一个简单的链表作为队列。在这些情况下,扫描现有插入的队列是 O (N) ,您可能不希望在每次迭代中都这样做。

在这种情况下,跟踪已经获得的锁是非常昂贵的。假设您希望在节点级别执行锁定,那么可重入锁定机制可以减少告诉您以前是否访问过某个节点的需要。您可以只是盲目地锁定该节点,或者在将其从队列中弹出之后解锁它。

可重入的互斥对象

一个简单的互斥对象是不可重入的,因为在给定的时间只有一个线程可以在关键部分。如果您抓取互斥对象,然后再次尝试抓取它,那么一个简单的互斥对象没有足够的信息来告诉您之前是谁持有它。为了递归地执行此操作,您需要一种机制,其中每个线程都有一个令牌,这样您就可以知道是谁抓住了互斥对象。这使得互斥机制有些昂贵,因此您可能不希望在所有情况下都这样做。

IIRC POSIX 线程 API 确实提供了可重入和非可重入互斥对象的选项。

可重入锁使您可以编写一个方法 M,该方法将锁放在资源 A上,然后递归地调用 M,或者从已经在 A上持有锁的代码中调用 M

对于一个不可重入的锁,您需要两个版本的 M,一个可以锁定,一个不能锁定,还需要额外的逻辑来调用正确的那个。

可重入锁在这个 教程中得到了很好的描述。

本教程中的示例远不如有关遍历图的答案中的示例那么做作。可重入锁在非常简单的情况下非常有用。

递归互斥量递归互斥量是什么以及为什么不应该是在公认的答案中描述的如此复杂的事情。

我想写下我的理解后,一些挖掘周围的网络。


首先,您应该意识到,在谈论 互斥锁时,肯定也涉及到多线程的概念。(互斥对象用于同步。如果程序中只有1个线程,则不需要互斥对象)


其次,你应该知道 正常的互斥体递归互斥量递归互斥量之间的区别。

引自 阿普:

(递归互斥锁是 a)允许 同样的线锁定的互斥锁类型 它多次没有首先解锁。

关键区别在于 在同一条线上,重新锁定一个递归锁定不会导致死锁,也不会阻塞线程。

这是否意味着回退锁永远不会导致死锁?
不,如果您将它锁定在一个线程中而没有解锁它,并尝试将它锁定在其他线程中,那么它仍然可以像普通互斥锁一样导致死锁。

让我们看看一些代码作为证据。

  1. 带死锁的正常互斥锁
#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

常见的死锁示例,没有问题。

  1. 带死锁的递归互斥锁

只要删除这一行的注释即可
error = pthread_mutexattr_settype(&lock_attr, PTHREAD_MUTEX_RECURSIVE);
然后评论另一个。

产出:

thread1
thread1 hey hey
thread2

是的,递归互斥也会导致死锁。

  1. 普通互斥锁,在同一个线程中重新锁定
#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 t1func3陷入僵局。
(我使用 sleep(2)来更容易看到死锁首先是由 func3中的重锁引起的)

  1. 递归互斥,在同一个线程中重新锁定

同样,取消递归互斥对象行的注释,并注释掉另一行。

产出:

thread1
func3
func3 hey hey
thread1 hey hey
thread2

thread t2中的死锁,func2中的死锁。看到了吗? func3完成并退出,重锁不会阻塞线程或导致死锁。


最后一个问题,我们为什么需要它?

对于递归函数(在多线程程序中调用,并希望保护一些资源/数据)。

例如。您有一个多线程程序,并在线程 A 中调用一个递归函数。在那个递归函数中有一些需要保护的数据,所以使用互斥机制。该函数的执行在线程 A 中是顺序的,所以在递归中一定要重新锁定互斥锁。使用普通互斥体会导致死锁。复活互斥系统(互斥系统)就是为了解决这个问题而发明的。

看一个例子从接受的答案 何时使用递归互斥锁。

维基百科很好地解释了递归互斥对象。绝对值得一读