Чому pthread_cond_wait має помилкові пробудження?


145

Щоб процитувати сторінку людини:

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

Отже, pthread_cond_waitможе повернутися, навіть якщо ви цього не сигналізували. Принаймні, на перший погляд, це здається досить жорстоким. Це було б як функція, яка випадковим чином повертає неправильне значення або випадково повертається до того, як фактично дійшла до правильного оператора return. Це здається великою помилкою. Але той факт, що вони вирішили задокументувати це на сторінці "man", а не виправляти, це, мабуть, свідчить про те, що є законна причина, чому в pthread_cond_waitкінцевому підсумку прокидаються неправдиво. Імовірно, є щось суттєве у тому, як це працює, що робить це таким чином, що це не може допомогти. Питання - що.

Чому ж pthread_cond_waitповернутися підроблено? Чому він не може гарантувати, що він прокинеться лише тоді, коли буде правильно сигналізовано? Чи може хтось пояснити причину своєї хибної поведінки?


5
Я думаю, що це має відношення до повернення кожного разу, коли процес ловить сигнал. Більшість * nixes не запускають виклик, що блокує, після того, як сигнал перериває його; вони просто встановлюють / повертають код помилки, який говорить про те, що стався сигнал.
cHao

1
@cHao: хоча зауважте, що через те, що змінні умови мають інші причини для помилкових пробуджень, обробка сигналу не є помилкою для pthread_cond_(timed)wait: "Якщо сигнал подається ..., потік продовжується в очікуванні змінної умови, як ніби він був не переривається, або він поверне нуль через помилкове пробудження ". Інші функції блокування вказують, EINTRколи їх перериває сигнал (наприклад read), або потрібно відновити їх (наприклад pthread_mutex_lock). Тож якби не було інших причин помилкового пробудження, pthread_cond_waitможна було б визначити як будь-яку з цих.
Стів Джессоп

4
Пов'язана стаття у Вікіпедії:
Лжеві


Багато функцій не можуть повністю виконати свою роботу (перерваний введення / вивід), і функції спостереження можуть отримувати такі події, як зміна каталогу, де зміну було скасовано або повернено назад. В чому проблема?
curiousguy

Відповіді:


77

Наступне пояснення дає Девід Р. Бутенхоф у "Програмуванні за допомогою ниток POSIX" (стор. 80):

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

У наступному обговоренні comp.programming.threads він розширює питання мислення, що стоїть за проектом:

Патрік Дойл написав: 
> У статті Том Пейн писав: 
>> Каз Кілхеку написав: 
>>: Це так тому, що іноді реалізація не може уникнути вставки 
>>: ці хибні пробудження; запобігти їх може бути дорого.

>> Але чому? Чому це так складно? Наприклад, чи говоримо ми
>> ситуації, коли очікує час очікування, як тільки надходить сигнал? 

> Знаєте, мені цікаво, чи дизайнери pthreads використовували таку логіку: 
> користувачі змінних умов повинні перевірити умову при виході, 
> тож ми не будемо покладати на них додаткового тягаря, якщо дозволимо 
> помилкові пробудження; і оскільки можливо, що допускати помилкові
> Пробудження може зробити реалізацію швидше, це може допомогти лише якщо ми 
> дозволити їх. 

> Вони, можливо, не мали на увазі якоїсь конкретної реалізації. 

Ти насправді зовсім не за горами, за винятком того, що ти не підштовхнув його досить далеко. 

Намір полягав у тому, щоб застосувати правильний / надійний код, вимагаючи предикатних циклів. Це було
керований напевно правильним академічним контингентом серед "основних ниток" в Росії 
Робоча група, хоча я не думаю, що хтось дійсно не погодився з наміром 
раз зрозуміли, що це означає. 

Ми дотримувалися цього наміру з кількома рівнями обґрунтування. Перше було те
"релігійно" за допомогою циклу захищає додаток від власного недосконалого 
практики кодування. Друге було те, що не важко було абстрактно уявити
машини та код реалізації, які могли б використовувати цю вимогу для вдосконалення 
виконання середніх умов очікування операцій через оптимізацію 
механізми синхронізації. 
/ ------------------ [David.Buten ... @ compaq.com] ------------------ \ 
| Compaq Computer Corporation POSIX Архітектор ниток |
| Моя книга: http://www.awl.com/cseng/titles/0-201-63392-2/ |
\ ----- [http://home.earthlink.net/~anneart/family/dave.html] ----- / 


22
в основному це нічого не говорить. Тут не дається жодних пояснень, окрім початкової думки, що "це може зробити все швидше", але ніхто не знає, як чи взагалі це відбувається.
Богдан Іоніца

107

Існують щонайменше дві речі, які «помилкові пробудження» можуть означати:

  • Нитка, заблокована в, pthread_cond_waitможе повертатися з виклику, навіть якщо жодного дзвінка до pthread_call_signalабо pthread_cond_broadcastза умови не відбулося.
  • Нитка, заблокована у pthread_cond_waitзворотах через виклик pthread_cond_signalабо pthread_cond_broadcast, однак після повторного придбання мютексу, виявляється, що основний предикат вже не відповідає дійсності.

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

  • Нитка 1 щойно видала елемент і випустила мютекс, і черга тепер порожня. Потік робить все, що робить з елементом, який він придбав на якомусь процесорі.
  • Thread 2 намагається видалити елемент, але виявляє, що черга буде порожньою, коли вона перевіряється під mutex, дзвінками pthread_cond_waitта блоками в дзвінку, що очікує сигналу / трансляції.
  • Нитка 3 отримує mutex, вставляє новий елемент у чергу, повідомляє змінну стану та звільняє замок.
  • У відповідь на повідомлення з потоку 3 планується запустити потік 2, який чекав за умови.
  • Однак перед тим, як потоку 2 вдається потрапити на процесор і захопити замок черги, потік 1 виконує своє поточне завдання і повертається до черги для більшої роботи. Він отримує блокування черги, перевіряє присудок і виявляє, що в черзі є робота. Він переходить до видалення елемента, у який вставлена ​​нитка 3, звільняє замок і робить все, що робиться з тим елементом, який запускається нитка 3.
  • Нитка 2 тепер потрапляє на процесор і отримує блокування, але коли він перевіряє присудок, він виявляє, що черга порожня. Нитка 1 "вкрала" предмет, тому пробудження виявляється помилковим. Нитку 2 потрібно знову зачекати на умові.

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


23
так. По суті, це те, що відбувається, коли подія використовується замість механізму синхронізації з підрахунком. Сумно, але, здається, що семафори POSIX (у будь-якому випадку в Linux) також підлягають пробудженню spurius. Мені просто здається дивним, що принциповий збій функціональних примітивів для синхронізації просто приймається як "звичайний" і його потрібно обробляти на рівні користувача :( Імовірно, розробники будуть готовими, якби системний виклик був задокументований з розділом «паразитні» або видадуть помилку сегментації, можливо , «Помилкові підключення до неправильного URL» або «паразитного відкритті невірного файлу».
Мартін Джеймс

2
Більш поширений сценарій "помилкового пробудження", швидше за все, є побічним ефектом виклику pthread_cond_broadcast (). Скажімо, у вас є пул з 5 ниток, дві прокидаються до трансляції і виконують роботу. Інші троє прокидаються і знаходять, що робота була зроблена. Багатопроцесорні системи також можуть спричинити умовний сигнал випадковим пробудженням декількох потоків. Код просто перевіряє предикат знову, бачить недійсний стан і повертається до сну. В будь-якому випадку перевірка присудка вирішує проблему. IMO, як правило, користувачі не повинні використовувати сирі тексти POSIX та умовні умови.
CubicleSoft

1
@MartinJames - Як щодо класичного "хибного" EINTR? Я погоджуся, що постійно тестувати EINTR у циклі - це дуже дратує і робить код досить некрасивим, але розробники роблять це так чи інакше, щоб уникнути випадкових поломок.
CubicleSoft

2
@Yola Ні, це не може, тому що ви повинні заблокувати мютекс навколо, pthread_cond_signal/broadcastі ви не зможете цього зробити, поки мютекс не буде розблокований, зателефонувавши pthread_cond_wait.
a3f

1
Приклад цієї відповіді дуже реалістичний, і я погоджуюся, що перевірка предикатів є хорошою ідеєю. Однак, чи не вдалося це виправити однаково надійно, зробивши проблемний крок, "нитка 1 завершує своє поточне завдання і повертається в чергу для додаткової роботи", а заміна її на "потік 1 завершує поточне завдання" і повертається назад, щоб чекати на змінна умова "? Це усуне режим відмови, описаний у відповіді, і я впевнений, що це зробить код правильним, за відсутності помилкових пробуджень . Чи є реальна реалізація, яка виробляє помилкові пробудження на практиці?
Квомплусон

7

У розділі "Кілька пробуджень за сигналом стану" в pthread_cond_signal є приклад реалізації pthread_cond_wait та pthread_cond_signal, який передбачає помилкові пробудження.


2
Я вважаю, що ця відповідь є неправильною, наскільки це стосується. Зразок реалізації на цій сторінці має реалізацію "notify one", що еквівалентно "сповістити всіх"; але це, здається, не генерує насправді помилкових пробуджень. Єдиний спосіб, щоб потік прокинувся - це якийсь інший потік, який викликає "сповістити всіх", або якийсь інший потік, який викликає тему-річ-мічений- "сповістити одне" -що-це-справді- "сповістити всіх".
Quuxplusone

5

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

Ключова проблема полягає в тому, що якщо потік діє на скасування під час блокування pthread_cond_wait, побічні ефекти повинні бути такими, ніби він не споживає жодного сигналу змінної стану. Однак важко (і дуже обмежує) переконатися, що ви вже не споживали сигнал, коли ви починаєте діяти на скасування, і на цьому етапі неможливо "повторно" розмістити сигнал до змінної умови, оскільки ви можете опинитися в ситуації, коли абонент pthread_cond_signalвже виправданий, що зруйнував кондивар і звільнив пам'ять, в якій він проживав.

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

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