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

时间:2009-08-21 14:22:12

标签: multithreading locking pthreads

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

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

互斥是否重入?

4 个答案:

答案 0 :(得分:136)

可重入锁定

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

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

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

可重入锁定的用例

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

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

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

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

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

可重入的互斥锁

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

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

答案 1 :(得分:19)

可重入锁定允许您编写方法M,锁定资源A,然后递归调用M或从{{1}锁定代码}}。

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

答案 2 :(得分:12)

tutorial中详细描述了可重入锁定。

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

答案 3 :(得分:0)

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

我想在网络上挖一些东西后写下我的理解。


首先,您应该认识到在谈论 mutex 时,肯定也涉及多线程概念。 (互斥锁用于同步。如果程序中只有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);

}

输出:

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中是顺序执行的,因此您一定要以递归方式重新锁定互斥锁。使用普通互斥锁会导致死锁。发明了递归互斥锁来解决这个问题。

从接受的答案中查看示例 When to use recursive mutex?

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