什么是可重入锁定和概念?

veh*_*zzz 81 multithreading locking pthreads

我总是感到困惑.有人会解释Reentrant在不同背景下的含义吗?为什么要使用折返与非折返?

说pthread(posix)锁定原语,它们是否可以重入?使用它们时应该避免哪些陷阱?

互斥是重入的吗?

Con*_*lls 143

可重入锁定

可重入锁是指进程可以多次声明锁而不会阻塞自身的锁.在跟踪你是否已经抓住锁定并不容易的情况下,它非常有用.如果一个锁是不可重入的,你可以抓住锁,然后在再次抓住它时阻塞,有效地使你自己的进程陷入僵局.

一般的重入是代码的属性,它没有中心可变状态,如果代码在执行时被调用,则可能会被破坏.这样的调用可以由另一个线程进行,或者它可以由源自代码本身的执行路径递归地进行.

如果代码依赖于可以在执行过程中更新的共享状态,那么它不是可重入的,至少在该更新可能会破坏它时是不可重入的.

用于重入锁定的用例

可重入锁定的应用程序的一个(有点通用和做作)示例可能是:

  • 你有一些涉及遍历图形的算法的计算(可能有周期).由于循环或由于到同一节点的多个路径,遍历可以多次访问同一节点.

  • 数据结构受并发访问的限制,可能由于某种原因(可能是另一个线程)进行更新.您需要能够锁定单个节点以处理由于竞争条件导致的潜在数据损坏.出于某种原因(可能是性能),您不希望全局锁定整个数据结构.

  • 您的计算无法保留您访问过的节点的完整信息,或者您使用的数据结构不允许"我以前在这里"问题可以快速回答.

    这种情况的一个例子是Dijkstra算法的简单实现,优先级队列实现为二进制堆,或者使用简单链接列表作为队列进行广度优先搜索.在这些情况下,扫描队列中的现有插入是O(N),您可能不希望在每次迭代时都这样做.

在这种情况下,跟踪您已经获得的锁是昂贵的.假设您希望在节点级别执行锁定,则可重入锁定机制可以减少判断您之前是否访问过节点的需要.您可以盲目地锁定节点,也许在将其从队列中弹出后将其解锁.

可重入的互斥量

一个简单的互斥锁不是可重入的,因为在给定时间只有一个线程可以位于临界区.如果你抓住互斥锁,然后尝试再次抓取它,一个简单的互斥锁没有足够的信息来告诉谁以前持有它.要以递归方式执行此操作,您需要一种机制,其中每个线程都有一个令牌,以便您可以告诉谁抓住了互斥锁.这使得互斥机制稍微昂贵,因此您可能不希望在所有情况下都这样做.

IIRC POSIX线程API确实提供了可重入和不可重入的互斥锁的选项.

  • 尽管通常无论如何都应避免这种情况,因为这样也很难避免死锁。无论如何,线程化都足够困难,而不必怀疑您是否已锁定。 (2认同)

Hen*_*man 20

可重入锁定允许您编写一个方法M,该方法锁定资源A,然后M递归调用或从已持有锁定的代码调用A.

使用非可重入锁定,您将需要2个版本M,一个锁定,一个不锁定,另外一个逻辑可以调用正确的锁定.


小智 13

教程中详细描述了可重入锁定.

本教程中的示例远比关于遍历图形的答案要少.在非常简单的情况下,重入锁是有用的.


Ric*_*ick 6

递归互斥体的内容和原因不应该是公认答案中描述的如此复杂的事情。

我想在网上查了一些资料后写下我的理解。


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


其次,您应该知道普通互斥体递归互斥体之间的区别。

引用自APUE

(递归互斥体是 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);

}
Run Code Online (Sandbox Code Playgroud)

输出:

thread1
thread1 hey hey
thread2
Run Code Online (Sandbox Code Playgroud)

常见的死锁例子,没问题。

  1. 带有死锁的递归互斥锁

只需取消注释这一行
error = pthread_mutexattr_settype(&lock_attr, PTHREAD_MUTEX_RECURSIVE);
并注释掉另一行即可。

输出:

thread1
thread1 hey hey
thread2
Run Code Online (Sandbox Code Playgroud)

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

  1. 普通互斥体,在同一线程中重新锁定
thread1
thread1 hey hey
thread2
Run Code Online (Sandbox Code Playgroud)

输出:

thread1
func3
thread2
Run Code Online (Sandbox Code Playgroud)

陷入僵局thread t1,在func3
(我用sleep(2)它来更容易地看出死锁首先是由重新锁定引起的func3

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

再次取消注释递归互斥体行并注释掉另一行。

输出:

thread1
func3
func3 hey hey
thread1 hey hey
thread2
Run Code Online (Sandbox Code Playgroud)

陷入僵局thread t2,在func2。看?func3完成并退出,重新锁定不会阻塞线程或导致死锁。


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

对于递归函数(在多线程程序中调用并且您想要保护某些资源/数据)。

例如,您有一个多线程程序,并在线程 A 中调用递归函数。您想在该递归函数中保护一些数据,因此您使用互斥机制。该函数在线程 A 中的执行是顺序的,因此您肯定会在递归中重新锁定互斥体。使用普通互斥锁会导致死锁。递归互斥锁就是为了解决这个问题而发明的。

查看已接受答案中的示例 何时使用递归互斥体?

维基百科很好地解释了递归互斥体。绝对值得一读。维基百科:可重入互斥体