Что такое Re-entrant блокировка и концепция в целом? - PullRequest
78 голосов
/ 21 августа 2009

Я всегда запутываюсь. Кто-нибудь объяснит, что означает Reentrant в разных контекстах? И почему вы хотите использовать реентеративный или не реентерабельный?

Скажите pthread (posix) блокирующие примитивы, они повторные или нет? Какие подводные камни следует избегать при их использовании?

Является ли мьютекс вновь входящим?

Ответы [ 4 ]

137 голосов
/ 21 августа 2009

Блокировка повторного входа

Блокировка повторного входа - это та, в которой процесс может требовать блокировку несколько раз, не блокируя сам себя. Это полезно в ситуациях, когда нелегко отслеживать, захватили ли вы уже блокировку. Если блокировка не повторяется, вы можете захватить блокировку, а затем заблокировать ее, чтобы снова захватить, эффективно блокируя собственный процесс.

Повторяемость в общем случае - это свойство кода, в котором отсутствует центральное изменяемое состояние, которое может быть повреждено, если код был вызван во время его выполнения. Такой вызов может быть сделан другим потоком, или он может быть выполнен рекурсивно с помощью пути выполнения, исходящего из самого кода.

Если код полагается на общее состояние, которое может быть обновлено в середине его выполнения, он не возвращается, по крайней мере, если это обновление может его сломать.

Вариант использования для повторной блокировки входа

(несколько универсальным и надуманным) примером приложения для блокировки повторного входа может быть:

  • У вас есть некоторые вычисления, включающие алгоритм, который пересекает граф (возможно, с циклами в нем). Обход может посещать один и тот же узел более одного раза из-за циклов или из-за нескольких путей к одному и тому же узлу.

  • Структура данных является объектом одновременного доступа и может быть обновлена ​​по какой-то причине, возможно, другим потоком. Вы должны иметь возможность блокировать отдельные узлы, чтобы справиться с возможным повреждением данных из-за состязаний. По какой-то причине (возможно, производительности) вы не хотите глобально блокировать всю структуру данных.

  • Вы не можете сохранить полную информацию о том, какие узлы вы посетили, или вы используете структуру данных, которая не позволяет быстро ответить на вопросы «Я был здесь раньше».

    Примером такой ситуации может быть простая реализация алгоритма Дейкстры с очередью приоритетов, реализованной в виде двоичной кучи, или поиск в ширину с использованием простого связанного списка в качестве очереди. В этих случаях сканирование очереди на наличие существующих вставок выполняется за O (N), и вам может не потребоваться делать это на каждой итерации.

В этой ситуации отслеживать, какие блокировки вы уже приобрели, дорого. Предполагая, что вы хотите выполнить блокировку на уровне узла, механизм повторной блокировки устраняет необходимость сообщать, посещали ли вы ранее узел. Вы можете просто слепо заблокировать узел, возможно, разблокировать его после того, как вы удалите его из очереди.

Повторный мьютекс

Простой мьютекс не является повторным входом, так как в определенный момент времени в критическом разделе может находиться только один поток. Если вы захватите мьютекс, а затем попытаетесь захватить его снова, у простого мьютекса не будет достаточно информации, чтобы сказать, кто удерживал его ранее. Чтобы сделать это рекурсивно, вам нужен механизм, в котором у каждого потока есть токен, чтобы вы могли определить, кто захватил мьютекс. Это делает мьютексный механизм несколько более дорогим, поэтому вам может не понадобиться делать это во всех ситуациях.

IIRC API потоков POSIX предлагает возможность повторного входа и не повторного входа мьютексов.

19 голосов
/ 21 августа 2009

Повторная блокировка позволяет вам написать метод M, который устанавливает блокировку для ресурса A и затем рекурсивно вызывать M или из кода, который уже удерживает блокировку на A.

При блокировке без повторного входа вам потребуется 2 версии M, одна из которых блокирует, а другая нет, и дополнительная логика для вызова правильной.

13 голосов
/ 05 марта 2014

Блокировка повторного входа очень хорошо описана в этом учебнике .

Пример в руководстве гораздо менее надуманный, чем в ответе о прохождении графа. Блокировка повторного входа полезна в очень простых случаях.

0 голосов
/ 15 апреля 2019

Что и почему рекурсивный мьютекс не должен быть такой сложной вещью, описанной в принятом ответе.

Я бы хотел записать свое понимание после того, как покопался в сети.


Во-первых, вы должны понимать, что когда речь идет о мьютекс , определенно участвуют также многопоточные концепции. (мьютекс используется для синхронизации. Мне не нужен мьютекс, если в моей программе только 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 t1, в func3.
(Я использую sleep(2), чтобы было легче увидеть, что в первую очередь тупик вызван повторной блокировкой в ​​func3)

  1. рекурсивный мьютекс, блокировка в том же потоке

Опять же, раскомментируйте строку рекурсивного мьютекса и закомментируйте другую строку.

вывод:

thread1
func3
func3 hey hey
thread1 hey hey
thread2

тупик в thread t2, в func2. Увидеть? func3 завершается и завершается, повторная блокировка не блокирует поток и не приводит к тупику.


Итак, последний вопрос, зачем нам это нужно?

Для рекурсивной функции (вызывается в многопоточных программах и вы хотите защитить некоторые ресурсы / данные).

например. У вас есть многопоточная программа, и вы вызываете рекурсивную функцию в потоке A. У вас есть некоторые данные, которые вы хотите защитить в этой рекурсивной функции, поэтому вы используете механизм мьютекса. Выполнение этой функции является последовательным в потоке A, поэтому вы обязательно заблокируете мьютекс в рекурсии. Использование нормального мьютекса вызывает тупики. И Resursive Mutex изобретен, чтобы решить эту проблему.

см. Пример из принятого ответа Когда использовать рекурсивный мьютекс? .

Википедия очень хорошо объясняет рекурсивный мьютекс. Определенно стоит почитать. Википедия: Reentrant_mutex

...