Я читаю далі pthread.h
; функції, пов'язані зі змінною умови (як pthread_cond_wait(3)
), потребують mutex як аргумент. Чому? Наскільки я можу сказати, я буду створювати мютекс просто для використання в якості аргументу? Що це мутекс?
Я читаю далі pthread.h
; функції, пов'язані зі змінною умови (як pthread_cond_wait(3)
), потребують mutex як аргумент. Чому? Наскільки я можу сказати, я буду створювати мютекс просто для використання в якості аргументу? Що це мутекс?
Відповіді:
Це лише те, як змінені умови (або були спочатку) реалізовані.
Мутекс використовується для захисту самої змінної стану . Ось чому його потрібно заблокувати, перш ніж зачекати.
Очікування "атомно" розблокує мютекс, дозволяючи іншим доступ до змінної умови (для сигналізації). Тоді, коли змінна стану передається на сигнал або транслюється, одна чи більше ниток у списку очікування буде пробуджено, і мютекс знову буде магічно заблокований для цього потоку.
Зазвичай ви бачите наступну операцію зі змінними стану, ілюструючи їх роботу. Наступний приклад - це робоча нитка, якій задається робота через сигнал до змінної стану.
thread:
initialise.
lock mutex.
while thread not told to stop working:
wait on condvar using mutex.
if work is available to be done:
do the work.
unlock mutex.
clean up.
exit thread.
Робота виконується в рамках цього циклу за умови, що є кілька доступних, коли очікування повернеться. Коли потік позначено, щоб перестати виконувати роботу (зазвичай інший потік встановлює умову виходу, потім натискає змінну умови, щоб пробудити цю нитку), цикл вийде, мютекс буде розблокований і ця нитка вийде.
Код, наведений вище, - модель для споживачів, оскільки мютекс залишається заблокованим під час роботи. Для варіантів для кількох споживачів ви можете використовувати, як приклад :
thread:
initialise.
lock mutex.
while thread not told to stop working:
wait on condvar using mutex.
if work is available to be done:
copy work to thread local storage.
unlock mutex.
do the work.
lock mutex.
unlock mutex.
clean up.
exit thread.
що дозволяє іншим споживачам отримувати роботу, поки цей працює.
Змінна умови звільняє вас від тягаря опитування якоїсь умови, а не дозволяє іншому потоку сповіщати вас про те, що щось має відбутися. Інший потік може сказати, що нитка, яка працює, доступна наступним чином:
lock mutex.
flag work as available.
signal condition variable.
unlock mutex.
Переважна більшість того, що часто помилково називають хибними пробудженнями, як правило, завжди тому, що під час їх pthread_cond_wait
дзвінка (трансляції) було передано декілька потоків , можна повернутися з мютексом, виконати роботу, а потім знову почекати.
Тоді друга сигнальна нитка могла вийти, коли не було роботи. Тож вам довелося мати додаткову змінну, яка вказує на те, що слід виконувати роботу (це було по суті захищено mutex за допомогою пари condvar / mutex тут - інші потоки, необхідні для блокування мютексу, перш ніж змінювати його).
Це було технічно можливо для нитки , щоб повернутися зі стану очікування , не виганяють інший процес (це є справжнім підробленим будильником) , але у всіх мої багатьох роках працюють над Pthreads, як у розвитку послуг / коди і як користувач з них я жодного разу не отримував жодного з них. Можливо, це було лише тому, що HP мала гідну реалізацію :-)
У будь-якому випадку той самий код, який обробляв помилковий випадок, також обробляв справжні помилкові пробудження, оскільки прапор, доступний для роботи, не буде встановлений для них.
do something
всередині while
петлі?
Змінна умови є досить обмеженою, якщо ви могли подавати сигнал лише про стан, зазвичай вам потрібно обробити деякі дані, пов'язані з умовою, про яку було подано сигнал. Сигналізація / пробудження повинні здійснюватися атомним шляхом, щоб досягти цього, не вводячи гоночні умови, або бути надмірно складними
pthreads також може дати вам з досить технічних причин хибну пробудження . Це означає, що вам потрібно перевірити присудок, щоб ви могли бути впевнені, що стан насправді був сигналізований - і відрізняти це від хибного пробудження. Перевірку такої умови щодо очікування її потрібно захистити - тому змінна умова потребує способу атомного очікування / пробудження під час блокування / розблокування мютексу, що захищає цю умову.
Розглянемо простий приклад, коли ви отримуєте сповіщення про отримання деяких даних. Можливо, інший потік зробив потрібні вам дані та встановив покажчик на ці дані.
Уявіть собі, що виробник передає деякі дані іншому потоку споживача через покажчик 'some_data'.
while(1) {
pthread_cond_wait(&cond); //imagine cond_wait did not have a mutex
char *data = some_data;
some_data = NULL;
handle(data);
}
Ви, природно, отримаєте велику кількість перегонів, що робити, якщо інший потік зробив some_data = new_data
відразу після того, як ви прокинулися, але перед тим, як виdata = some_data
Ви не можете створити власний файл mutex для захисту цієї справи .eg
while(1) {
pthread_cond_wait(&cond); //imagine cond_wait did not have a mutex
pthread_mutex_lock(&mutex);
char *data = some_data;
some_data = NULL;
pthread_mutex_unlock(&mutex);
handle(data);
}
Не вийде, все ж є ймовірність перегону між пробудженням та захопленням мютексу. Розміщення mutex перед pthread_cond_wait не допоможе вам, оскільки ви тепер будете тримати мутекс під час очікування - тобто виробник ніколи не зможе схопити файли. (зауважте, у цьому випадку ви можете створити другу змінну умови, щоб сигналізувати виробнику, що ви закінчили some_data
- хоча це стане складним, особливо, якщо ви хочете багатьох виробників / споживачів.)
Таким чином, вам потрібен спосіб атомного звільнення / захоплення мютексу під час очікування / пробудження із стану. Ось що робить змінні умови pthread, і ось що ви зробите:
while(1) {
pthread_mutex_lock(&mutex);
while(some_data == NULL) { // predicate to acccount for spurious wakeups,would also
// make it robust if there were several consumers
pthread_cond_wait(&cond,&mutex); //atomically lock/unlock mutex
}
char *data = some_data;
some_data = NULL;
pthread_mutex_unlock(&mutex);
handle(data);
}
(виробник, природно, повинен дотримуватися тих самих запобіжних заходів, завжди захищаючи "some_data" з однаковим файлом і переконуючись, що він не перезаписає some_data, якщо some_data зараз є = = NULL)
while (some_data != NULL)
бути цикл виконання часу, щоб він чекав змінної умови хоча б раз?
while(some_data != NULL)
бути while(some_data == NULL)
?
Змінні умови POSIX не мають стану. Отже, ви відповідальні за підтримку держави. Оскільки до стану стануть доступні як потоки, які чекають, так і потоки, які повідомляють іншим потокам припинити очікування, вони повинні бути захищені mutex. Якщо ви думаєте, що можете використовувати змінні умови без mutex, тоді ви не зрозуміли, що змінні стану є без стану.
Змінні умови будуються навколо умови. Нитки, які чекають змінної умови, чекають певної умови. Нитки, що змінні стану сигналу, змінюють цю умову. Наприклад, нитка може чекати, коли деякі дані надійдуть. Деякі інші потоки можуть помітити, що дані надійшли. "Дані надійшли" - це умова.
Ось класичне використання змінної умови, спрощена:
while(1)
{
pthread_mutex_lock(&work_mutex);
while (work_queue_empty()) // wait for work
pthread_cond_wait(&work_cv, &work_mutex);
work = get_work_from_queue(); // get work
pthread_mutex_unlock(&work_mutex);
do_work(work); // do that work
}
Подивіться, як нитка чекає на роботу. Твір захищений мютекс. Очікування вивільняє mutex, щоб інша нитка могла дати цьому потоку деяку роботу. Ось як би сигналізували:
void AssignWork(WorkItem work)
{
pthread_mutex_lock(&work_mutex);
add_work_to_queue(work); // put work item on queue
pthread_cond_signal(&work_cv); // wake worker thread
pthread_mutex_unlock(&work_mutex);
}
Зауважте, що для захисту робочої черги потрібна mutex. Зауважте, що сама змінна умови не має поняття, є робота чи ні. Тобто змінна умова повинна бути пов’язана з умовою, цей стан повинен підтримуватися вашим кодом, а оскільки він поділяється між потоками, він повинен бути захищений мютекс.
Не всі функції змінної умови вимагають mutex: виконують лише операції очікування. Операції сигналу та трансляції не потребують мютексу. Змінна стану також не є постійно пов'язаною з певним мутексу; зовнішня мютекс не захищає змінну стану. Якщо змінна умови має внутрішній стан, такий як черга потоків очікування, це повинно бути захищено внутрішнім блокуванням всередині змінної умови.
Операції очікування об'єднують змінну стану та мютекс, оскільки:
З цієї причини операція очікування приймає аргументи як mutex, так і умову: так, щоб вона могла керувати атомним переносом потоку від володіння мютекс до очікування, щоб нитка не стала жертвою втраченого стану гонки пробудження .
Умова втрати в режимі пробудження відбудеться, якщо потік видасть мьютекс, а потім чекає на об'єкт синхронізації без стану, але таким чином, який не є атомним: існує вікно часу, коли в потоці більше немає блокування, і має ще не почали чекати на об’єкті. Під час цього вікна може вступити інший потік, зробити очікувану умову справжньою, подати сигнал про синхронізацію без стану, а потім зникнути. Об'єкт без громадянства не пам’ятає, що йому було подано сигнал (він без громадянства). Тоді початковий потік засинає на об'єкті синхронізації без стану, і не прокидається, хоча умова, яка йому потрібна, вже стала справжньою: втрачене пробудження.
Функції очікування змінної стану уникають втраченого пробудження, переконуючись, що викликова нитка зареєстрована для надійного вловлювання пробудження, перш ніж вона відмовиться від mutex. Це було б неможливо, якби функція очікування змінної умови не приймала mutex як аргумент.
Я не вважаю, що інші відповіді є такими ж стислими та читаними, як ця сторінка . Зазвичай код очікування виглядає приблизно так:
mutex.lock()
while(!check())
condition.wait()
mutex.unlock()
Існує три причини обгортання wait()
мутексу:
signal()
до цього, wait()
і ми пропустимо це пробудження.check()
залежить від модифікації з іншого потоку, тому вам все одно потрібно взаємне виключення.Третій момент не завжди викликає занепокоєння - історичний контекст пов'язаний із статті до цієї розмови .
Щодо цього механізму часто згадуються помилкові пробудження (тобто нитка очікування прокидається без виклику signal()
). Однак такі події обробляються петлею check()
.
Змінні умови пов'язані з мютекс, оскільки це єдиний спосіб уникнути гонки, якої він призначений уникати.
// incorrect usage:
// thread 1:
while (notDone) {
pthread_mutex_lock(&mutex);
bool ready = protectedReadyToRunVariable
pthread_mutex_unlock(&mutex);
if (ready) {
doWork();
} else {
pthread_cond_wait(&cond1); // invalid syntax: this SHOULD have a mutex
}
}
// signalling thread
// thread 2:
prepareToRunThread1();
pthread_mutex_lock(&mutex);
protectedReadyToRuNVariable = true;
pthread_mutex_unlock(&mutex);
pthread_cond_signal(&cond1);
Now, lets look at a particularly nasty interleaving of these operations
pthread_mutex_lock(&mutex);
bool ready = protectedReadyToRunVariable;
pthread_mutex_unlock(&mutex);
pthread_mutex_lock(&mutex);
protectedReadyToRuNVariable = true;
pthread_mutex_unlock(&mutex);
pthread_cond_signal(&cond1);
if (ready) {
pthread_cond_wait(&cond1); // uh o!
На даний момент немає жодної нитки, яка збирається сигналізувати про змінну умови, тому нитка1 буде чекати вічно, навіть якщо захищенийReadyToRunVariable каже, що готовий до запуску!
Єдиний шлях цього полягає в тому, щоб змінні умови атомно вивільняли мютекс, одночасно починаючи чекати змінної умови. Ось чому функція cond_wait вимагає mutex
// correct usage:
// thread 1:
while (notDone) {
pthread_mutex_lock(&mutex);
bool ready = protectedReadyToRunVariable
if (ready) {
pthread_mutex_unlock(&mutex);
doWork();
} else {
pthread_cond_wait(&mutex, &cond1);
}
}
// signalling thread
// thread 2:
prepareToRunThread1();
pthread_mutex_lock(&mutex);
protectedReadyToRuNVariable = true;
pthread_cond_signal(&mutex, &cond1);
pthread_mutex_unlock(&mutex);
Мютекс повинен бути заблокований під час дзвінка pthread_cond_wait
; коли ви називаєте це атомарно, обидва розблокує мютекс, а потім блокує умову. Після того, як умова буде сигналізована, вона атомно блокує її знову і повертається.
Це дозволяє реалізувати передбачуване планування за бажанням в тому, що потік, який би робив сигналізацію, може зачекати, поки мутекс звільниться, щоб виконати його обробку, а потім подати сигнал про стан.
Я зробив напругу в класі, якщо ви хочете реальний приклад змінної умови:
#include "stdio.h"
#include "stdlib.h"
#include "pthread.h"
#include "unistd.h"
int compteur = 0;
pthread_cond_t varCond = PTHREAD_COND_INITIALIZER;
pthread_mutex_t mutex_compteur;
void attenteSeuil(arg)
{
pthread_mutex_lock(&mutex_compteur);
while(compteur < 10)
{
printf("Compteur : %d<10 so i am waiting...\n", compteur);
pthread_cond_wait(&varCond, &mutex_compteur);
}
printf("I waited nicely and now the compteur = %d\n", compteur);
pthread_mutex_unlock(&mutex_compteur);
pthread_exit(NULL);
}
void incrementCompteur(arg)
{
while(1)
{
pthread_mutex_lock(&mutex_compteur);
if(compteur == 10)
{
printf("Compteur = 10\n");
pthread_cond_signal(&varCond);
pthread_mutex_unlock(&mutex_compteur);
pthread_exit(NULL);
}
else
{
printf("Compteur ++\n");
compteur++;
}
pthread_mutex_unlock(&mutex_compteur);
}
}
int main(int argc, char const *argv[])
{
int i;
pthread_t threads[2];
pthread_mutex_init(&mutex_compteur, NULL);
pthread_create(&threads[0], NULL, incrementCompteur, NULL);
pthread_create(&threads[1], NULL, attenteSeuil, NULL);
pthread_exit(NULL);
}
Це представляється конкретним дизайнерським рішенням, а не концептуальною потребою.
Згідно з документами pthreads причиною того, що мютекс не був відокремлений, є те, що завдяки їх поєднанню є значне поліпшення продуктивності, і вони очікують, що через загальні умови гонки, якщо ви не використовуєте мютекс, це майже завжди буде зроблено.
https://linux.die.net/man/3/pthread_cond_wait
Особливості Mutexes та змінних умов
Було запропоновано, щоб придбання та вивільнення мутексу було від'єднано від умови очікування. Це було відхилено, оскільки саме комбінований характер операції полегшує реалізацію в реальному часі. Ці реалізації можуть атомним чином переміщувати нитку з високим пріоритетом між змінною умови та mutex таким чином, що є прозорим для абонента. Це може запобігти додатковим контекстним комутаціям та забезпечити більш детерміноване придбання мютексу, коли сигналізується чекаюча нитка. Таким чином, питання справедливості та пріоритетності можуть вирішуватися безпосередньо дисципліною планування. Крім того, операція очікування поточного стану відповідає існуючій практиці.
Про це є багато екзегесів, але я хочу охарактеризувати це прикладом, наступним.
1 void thr_child() {
2 done = 1;
3 pthread_cond_signal(&c);
4 }
5 void thr_parent() {
6 if (done == 0)
7 pthread_cond_wait(&c);
8 }
Що не так з фрагментом коду? Просто поміркуйте, перш ніж рухатись вперед
Питання справді тонке. Якщо батько викликає
thr_parent()
і потім перевіряє значення done
, він побачить, що воно є, 0
і, таким чином, спробує перейти у сон. Але лише перед тим, як зателефонувати зачекати, щоб заснути, батько переривається між рядками 6-7, і дитина біжить. Дитина змінює змінну стану
, done
щоб 1
і сигнали, але ні один потік не чекає , і , таким чином , ні один потік не прокинувся. Коли батько знову біжить, він вічно спить, що насправді неприємно.
Що робити, якщо вони виконуються під час придбання замків індивідуально?