Що таке замок та концепція повторного вступу взагалі?


91

Я завжди плутаюся. Хтось пояснить, що означає " реентрант" у різних контекстах? І чому ви хочете використовувати реентерабельного та нереагентного?

Скажіть примітиви блокування pthread (posix), вони знову вступають чи ні? Яких підводних каменів слід уникати при їх використанні?

Мутекс знову входить?

Відповіді:


157

Блокування повторного вступу

Повторне блокування - це те, коли процес може вимагати блокування кілька разів, не блокуючи себе. Це корисно в ситуаціях, коли непросто відстежити, чи вже ви схопили замок. Якщо замок не є повторним учасником, ви можете схопити замок, а потім заблокуйте його, коли знову захопите, фактично заблокувавши власний процес.

Взаємозв'язок загалом є властивістю коду, де він не має центрального змінного стану, який може бути пошкоджений, якби код був викликаний під час його виконання. Такий виклик може бути зроблений іншим потоком, або він може бути здійснений рекурсивно шляхом виконання, що походить із самого коду.

Якщо код покладається на загальний стан, який може бути оновлений в середині його виконання, він не входить повторно, принаймні ні, якщо це оновлення може його зламати.

Приклад використання для блокування повторного вступу

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

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

  • Структура даних підлягає одночасному доступу і може бути оновлена ​​з якихось причин, можливо, за допомогою іншого потоку. Ви повинні мати можливість заблокувати окремі вузли, щоб впоратися з потенційними пошкодженнями даних через расові умови. З якоїсь причини (можливо, продуктивності) ви не хочете глобально блокувати всю структуру даних.

  • Ваше обчислення не може зберігати повну інформацію про те, які вузли ви відвідали, або ви використовуєте структуру даних, яка не дозволяє швидко відповісти на запитання "я вже був тут раніше".

    Прикладом такої ситуації може бути проста реалізація алгоритму Дейкстри з пріоритетною чергою, реалізованою як двійкова куча або пошук у ширину з використанням простого пов'язаного списку як черги. У цих випадках сканування черги для наявних вставних файлів має значення O (N), і ви можете не хотіти робити це на кожній ітерації.

У цій ситуації відстеження того, які замки ви вже придбали, є дорогим. Якщо припустити, що ви хочете зробити блокування на рівні вузла, механізм блокування повторного входу полегшує необхідність визначити, чи відвідували ви раніше вузол. Ви можете просто наосліп заблокувати вузол, можливо, розблокувавши його після того, як вискочите з черги.

Мютекси, що повторно вступають

Простий мьютекс не є повторним учасником, оскільки лише один потік може знаходитись у критичному розділі в даний момент часу. Якщо ви захопите мьютекс, а потім спробуєте захопити його ще раз, простий мьютекс не має достатньо інформації, щоб визначити, хто його тримав раніше. Для цього рекурсивно вам потрібен механізм, де кожен потік мав маркер, щоб ви могли зрозуміти, хто схопив мьютекс. Це робить мутексний механізм дещо дорожчим, тому ви можете не хотіти робити це у всіх ситуаціях.

IIRC API потоків POSIX дійсно пропонує опцію повторного входу та непід’їздних мьютексів.


2
Хоча таких ситуацій зазвичай слід уникати в будь-якому випадку, оскільки це ускладнює також уникнення тупикових ситуацій тощо. Нарізати різьблення в будь-якому випадку досить складно, не сумніваючись, чи вже маєте замок.
Джон Скіт,

+1, також розглянемо випадок, коли замок НЕ повертається, ви можете заблокувати себе, якщо не будете обережні. Плюс у C, у вас відсутні ті самі механізми, що й інші мови, щоб забезпечити випуск блокування стільки разів, скільки його придбано. Це може призвести до великих проблем.
user7116

1
це саме те, що трапилось зі мною вчора: я не взяв питання повторного вступу на розгляд і в кінцевому підсумку налагодив глухий кут протягом 5 годин ...
Vehomzzz

@Jon Skeet - Я думаю, що, ймовірно, бувають ситуації (див. Мій дещо надуманий приклад вище), коли відстеження замків недоцільно через продуктивність чи інші міркування.
ConcernedOfTunbridgeWells

21

Блокування повторного входу дозволяє вам написати метод, Mякий встановлює блокування ресурсу, Aа потім викликає Mрекурсивно або з коду, який уже містить блокування A.

З блокуванням, що не вступає, вам знадобляться 2 версії M, одна, яка блокується, і друга, яка цього не робить, а також додаткова логіка для виклику правильної.


Чи означає це, якщо у мене є рекурсивні виклики, що отримують один і той самий замок obj більше одного разу - скажімо xраз за даним потоком, я не можу чергувати виконання, не звільняючи всіх рекурсивно отриманих замків (той самий замок, але для xкількості разів)? Якщо це правда, то це по суті робить цю реалізацію послідовною. Мені чогось не вистачає?
DevdattaK

Це не повинно бути справжньою світовою проблемою. Це більше стосується гранульованого блокування та того, що Thread не замикається.
Хенк Холтерман,

16

Блокування реентранта дуже добре описано в цьому посібнику .

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


3

Що і чому рекурсивний мьютекс не повинен бути такою складною справою, описаною у прийнятій відповіді.

Я хотів би записати своє розуміння після певного копання в мережі.


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

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

Знову ж, розкомментируйте рекурсивний мютексний рядок і прокоментуйте інший рядок.

вихід:

thread1
func3
func3 hey hey
thread1 hey hey
thread2

Тупик у thread t2, в func2. Побачити? func3закінчує та виходить, повторне блокування не блокує нитку та не веде до глухого кута.


Отже, останнє запитання, навіщо це нам потрібно?

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

Наприклад, у вас є багатопотокова програма, і ви викликаєте рекурсивну функцію в потоці A. У вас є деякі дані, які ви хочете захистити в цій рекурсивній функції, тому ви використовуєте механізм mutex. Виконання цієї функції є послідовним у потоці A, тому ви точно перезаблокуєте мьютекс у рекурсії. Використання звичайного мьютексу спричиняє тупикові ситуації. І для вирішення цього винайдений ресурсивний мьютекс .

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

У Вікіпедії дуже добре пояснюється рекурсивний мьютекс. Безумовно, варто прочитати. Вікіпедія: Reentrant_mutex

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.