Повторная блокировка
Повторная блокировка - это блокировка, при которой процесс может требовать блокировку несколько раз, не блокируя себя. Это полезно в ситуациях, когда нелегко отследить, захватили ли вы блокировку. Если блокировка не повторяется, вы можете захватить блокировку, а затем заблокировать, когда вы пойдете, чтобы захватить ее снова, эффективно блокируя свой собственный процесс.
Повторная входимость в целом - это свойство кода, в котором нет центрального изменяемого состояния, которое могло бы быть повреждено, если код был вызван во время его выполнения. Такой вызов может быть выполнен другим потоком, или он может быть выполнен рекурсивно с помощью пути выполнения, происходящего из самого кода.
Если код полагается на общее состояние, которое может быть обновлено в середине его выполнения, он не является реентерабельным, по крайней мере, если это обновление могло его нарушить.
Пример использования повторной блокировки
Пример (несколько общий и надуманный) приложения для блокировки повторного входа может быть следующим:
У вас есть вычисления с использованием алгоритма, который проходит по графу (возможно, с циклами в нем). Обход может посещать один и тот же узел более одного раза из-за циклов или из-за нескольких путей к одному и тому же узлу.
Структура данных подлежит одновременному доступу и может быть обновлена по какой-то причине, возможно, другим потоком. Вы должны иметь возможность блокировать отдельные узлы, чтобы иметь дело с потенциальным повреждением данных из-за состояния гонки. По какой-то причине (возможно, из-за производительности) вы не хотите глобально блокировать всю структуру данных.
Ваше вычисление не может сохранить полную информацию о том, какие узлы вы посетили, или вы используете структуру данных, которая не позволяет быстро ответить на вопросы типа «был ли я здесь раньше».
Примером такой ситуации может быть простая реализация алгоритма Дейкстры с приоритетной очередью, реализованной как двоичная куча, или поиск в ширину с использованием простого связанного списка в качестве очереди. В этих случаях сканирование очереди на наличие существующих вставок выполняется за O (N), и вы можете не захотеть делать это на каждой итерации.
В этой ситуации отслеживать, какие замки вы уже приобрели, стоит дорого. Предполагая, что вы хотите выполнить блокировку на уровне узла, механизм повторной блокировки избавляет от необходимости определять, посещали ли вы узел раньше. Вы можете просто заблокировать узел вслепую, возможно, разблокировав его после того, как вытащите его из очереди.
Повторяющиеся мьютексы
Простой мьютекс не имеет повторного входа, так как только один поток может находиться в критической секции в данный момент времени. Если вы захватите мьютекс, а затем попытаетесь захватить его снова, у простого мьютекса не будет достаточно информации, чтобы определить, кто его ранее держал. Чтобы сделать это рекурсивно, вам нужен механизм, в котором у каждого потока был токен, чтобы вы могли определить, кто захватил мьютекс. Это делает механизм мьютекса несколько более дорогим, поэтому вы можете не захотеть использовать его во всех ситуациях.
IIRC API потоков POSIX предлагает возможность мьютексов с повторным входом и без него.
Блокировка повторного входа позволяет вам написать метод,
M
который устанавливает блокировку ресурса,A
а затем вызывает егоM
рекурсивно или из кода, который уже удерживает блокировкуA
.При блокировке без повторного входа вам потребуются 2 версии
M
, одна блокирующая, а другая нет, а также дополнительная логика для вызова нужной.источник
x
раз заданным потоком, я не могу чередовать выполнение, не освобождая все рекурсивно полученные блокировки (одна и та же блокировка, ноx
несколько раз)? Если это правда, то это по существу делает эту реализацию последовательной. Я что-то упускаю?Повторная блокировка очень хорошо описана в этом руководстве .
Пример в учебнике гораздо менее надуманный, чем в ответе о перемещении по графу. Повторная блокировка полезна в очень простых случаях.
источник
Что и почему рекурсивного мьютекса не должно быть такой сложной вещью, описанной в принятом ответе.
Я хотел бы записать свое понимание после некоторых поисков в сети.
Во-первых, вы должны понимать, что, говоря о мьютексах , определенно подразумевается и концепция многопоточности. (мьютекс используется для синхронизации. Мне не нужен мьютекс, если в моей программе только 1 поток)
Во-вторых, вы должны знать разницу между обычным мьютексом и рекурсивным мьютексом .
Цитируется из APUE :
Ключевое отличие состоит в том, что в одном потоке повторная блокировка рекурсивной блокировки не приводит к тупиковой ситуации и не блокирует поток.
Означает ли это, что повторная блокировка никогда не вызывает тупик?
Нет, он все равно может вызвать взаимоблокировку как обычный мьютекс, если вы заблокировали его в одном потоке, не разблокировав его, и попытались заблокировать его в других потоках.
Давайте посмотрим на код в качестве доказательства.
#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); }
выход:
общий пример тупика, нет проблем.
Просто раскомментируйте эту строку
error = pthread_mutexattr_settype(&lock_attr, PTHREAD_MUTEX_RECURSIVE);
и закомментируйте другую.
выход:
Да, рекурсивный мьютекс тоже может вызвать тупик.
#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); }
выход:
Тупик в
thread t1
, вfunc3
.(Я использую,
sleep(2)
чтобы было легче увидеть, что тупик в первую очередь вызван повторной блокировкойfunc3
)Снова раскомментируйте строку рекурсивного мьютекса и закомментируйте другую строку.
выход:
Тупик в
thread t2
, вfunc2
. Увидеть?func3
завершается и завершается, повторная блокировка не блокирует поток и не приводит к тупиковой ситуации.Итак, последний вопрос, зачем нам это нужно?
Для рекурсивной функции (вызываемой в многопоточных программах, и вы хотите защитить некоторые ресурсы / данные).
Например, у вас есть многопоточная программа и вы вызываете рекурсивную функцию в потоке A. У вас есть некоторые данные, которые вы хотите защитить в этой рекурсивной функции, поэтому вы используете механизм мьютекса. Выполнение этой функции является последовательным в потоке A, поэтому вы определенно заблокируете мьютекс в рекурсии. Использование обычного мьютекса вызывает взаимоблокировки. И для решения этой проблемы изобретен ресурсный мьютекс .
См. Пример из принятого ответа. Когда использовать рекурсивный мьютекс? .
Википедия очень хорошо объясняет рекурсивный мьютекс. Определенно стоит прочитать. Википедия: Reentrant_mutex
источник