什么是可重入锁及其概念?

133
我经常感到困惑。有人能解释一下在不同上下文中Reentrant的含义吗?为什么要使用可重入和非可重入?
例如pthread(posix)锁原语,它们是可重入的还是不可重入的?在使用它们时应避免哪些陷阱?
互斥锁是可重入的吗?
4个回答

207

可重入锁

可重入锁是一种进程可以多次申请锁而不会因此被阻塞的锁。它在难以跟踪是否已经获得锁的情况下非常有用。如果锁是不可重入的,你可能会先获得锁,然后在再次尝试获取锁时被阻塞,从而导致自己的进程陷入死锁。

可重入性通常是代码没有中央可变状态的特性,如果在执行代码时调用了该代码,就不会破坏任何东西。这样的调用可以由另一个线程进行,也可以由代码本身发起的执行路径递归进行。

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

可重入锁的使用场景

可重入锁的一个(有些泛化和牵强附会)示例应用程序可能是:

  • 您有一些计算涉及遍历图形的算法(可能具有其中的循环)。遍历可能会因为循环或多条到达同一节点的路径而多次访问同一节点。

  • 数据结构会受到并发访问,并且由于某些原因可能会进行更新,例如由另一个线程。您需要能够锁定单个节点以处理由于竞态条件引起的潜在数据损坏。出于某种原因(例如性能),您不想全局锁定整个数据结构。

  • 您的计算无法保留有关已访问的节点的完整信息,或者正在使用不允许快速回答“我之前是否来过”问题的数据结构。

    在这种情况下,一个简单的实现Dijkstra算法的例子是,使用二叉堆作为优先级队列,或使用简单的链表作为队列的广度优先搜索。在这些情况下,扫描队列以查找现有插入是O(N)的,您可能不想在每次迭代时执行它。

在这种情况下,跟踪已经获取的锁是昂贵的。假设您想在节点级别执行锁定,可重入锁定机制可以减轻需要告知是否访问过节点的需求。您只需盲目地锁定节点,可能在将其从队列中弹出后解锁。

可重入互斥锁

简单的互斥锁不是可重入的,因为在给定时间内只能有一个线程处于临界区域内。如果您抓住了互斥锁,然后尝试再次抓住它,则简单的互斥锁没有足够的信息告诉您之前谁持有它。要递归地执行此操作,需要一种机制,其中每个线程都有一个标记,以便您可以知道谁抓住了互斥锁。这使互斥锁机制变得更加昂贵,因此您可能不想在所有情况下都这样做。

我记得 POSIX 线程 API 提供了可重入和非可重入互斥锁选项。


2
虽然通常应该避免这种情况,因为这会使避免死锁等问题变得更加困难。线程编程本来就很难,如果你不确定是否已经获取了锁,那么就更加困难了。 - Jon Skeet
+1,还要考虑锁不可重入的情况,如果不小心可能会阻塞自己。此外,在C语言中,您没有其他语言所具备的机制来确保锁被释放的次数与获取的次数相同。这可能会导致严重问题。 - user7116
1
这正是昨天发生在我身上的事情:我没有考虑到重入问题,最终花了5个小时来调试死锁... - vehomzzz
@Jon Skeet - 我认为可能存在一些情况(请参见我上面有点牵强附会的例子),由于性能或其他考虑因素,跟踪锁定是不切实际的。 - ConcernedOfTunbridgeWells

26

可重入锁允许你编写一个方法M,它能够对资源A进行加锁,并且可以递归地调用M或从已经持有A锁的代码中调用M

使用不可重入锁,你需要编写两个版本的M,一个进行加锁,另一个不进行加锁,并且还需要额外的逻辑来调用正确的版本。


这是否意味着,如果我有递归调用在同一线程中多次获取相同的锁对象 - 比如说 x 次,我不能在不释放所有递归获取的锁(相同的锁但是获取了 x 次)的情况下交错执行?如果是这样,那么它本质上使得这个实现成为顺序执行。我有什么遗漏吗? - DevdattaK
这不应该是一个真正的世界问题。更多的是关于粒度锁定和线程不会将自身锁定。 - H H

21

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

该教程中的示例比遍历图形的回答要简单得多。可重入锁在非常简单的情况下是有用的。


6
递归互斥锁的定义及原因不应该像接受的答案中描述的那样复杂。经过一番搜索后,我想写下自己的理解。首先,你应该意识到讨论 互斥锁 时,绝对涉及多线程概念。(如果程序中只有一个线程,则不需要互斥锁进行同步) 其次,你应该知道普通互斥锁和递归互斥锁之间的区别。引用自 APUE : (递归互斥锁是)允许< strong>相同的线程多次锁定它而无需首先解锁它的互斥锁类型。关键区别在于,在< strong>同一个线程中,重新加锁递归锁不会导致死锁,也不会阻塞线程。这是否意味着递归锁永远不会导致死锁?不,如果您在一个线程中锁定了它而没有解锁它,并尝试在其他线程中锁定它,则仍然可能导致死锁,就像普通互斥锁一样。让我们看一些代码作为证明。
  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中,该函数的执行是顺序的,因此在递归中肯定会重新锁定互斥量。使用普通的互斥量会导致死锁,而可重入互斥量被发明来解决这个问题。

请参见接受答案中的示例:When to use recursive mutex?

维基百科非常好地解释了可重入互斥量。值得一读。Wikipedia: Reentrant_mutex


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接