一般而言,重入锁和概念是什么?


91

我总是很困惑。有人会解释可重入在不同情况下的含义吗?以及为什么要使用可重入与非可重入?

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

互斥锁是否重新进入?

Answers:


157

重入锁定

可重入锁是一种过程可以多次声明该锁而不会对其自身进行阻塞的锁。在难以跟踪您是否已经抓住锁的情况下,此功能很有用。如果某个锁是不可重入的,则可以抓住该锁,然后在再次获取时阻塞,从而有效地使自己的进程死锁。

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

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

重入锁定的用例

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

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

  • 数据结构可以进行并发访问,并且由于某种原因可能会被其他线程更新。您需要能够锁定单个节点以应对由于竞争条件而导致的潜在数据损坏。由于某种原因(也许是性能),您不想全局锁定整个数据结构。

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

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

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

重入互斥体

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

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


2
尽管通常无论如何都应避免这种情况,因为这样也很难避免死锁。无论如何,线程化都足够困难,而不必怀疑您是否已锁定。
乔恩·斯基特

+1,还请考虑该锁未重入的情况,如果不小心,可以阻止自己。另外,在C语言中,您没有其他语言可以确保释放锁的次数与获得锁的次数相同的机制。这可能会导致大问题。
user7116

1
这就是昨天我到底发生了什么:我没有考虑重新进入的问题,并最终调试了5个小时的死锁……
vehomzzz

@Jon Skeet-我认为在某些情况下(请参见上面我做作的例子),出于性能或其他考虑,跟踪锁是不切实际的。
ConcernedOfTunbridgeWells,2009年

21

使用重入锁,您可以编写一个M对资源加锁的方法,A然后M递归调用,或者从已经持有on的代码中进行递归调用A

使用非重入锁,您将需要2个版本的M,一个可以锁定的版本和一个不可以锁定的版本,以及其他用于调用正确版本的逻辑。


这是否意味着如果我的递归调用x多次获取同一锁obj(例如,通过给定线程获取x多次),那么我不能在不释放所有递归获取的锁的情况下交错执行(相同的锁,但是次数)?如果为true,则实质上使此实现顺序化。我想念什么吗?
DevdattaK

那不应该是一个真正的难题。它更多地与粒度锁定有关,并且线程不会将自身锁定。
Henk Holterman '18


3

递归互斥锁的内容和原因不应是公认的答案中描述的那么复杂的事情。

我想在网上进行一些探讨后写下我的理解。


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


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

引用APUE

(递归互斥锁是一种)互斥锁类型,它允许同一线程多次锁定它而无需先对其进行解锁。

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

这是否意味着后退锁定永远不会导致死锁?
不,如果您将死锁锁定在一个线程中但未将其解锁,然后尝试将其锁定在其他线程中,它仍然可能像普通互斥锁一样导致死锁。

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

  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 t2func2。看到?func3完成并退出后,重新锁定不会阻塞线程或导致死锁。


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

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

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

请参阅已接受答案中的示例 何时使用递归互斥锁?

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.