Чому для змінних функцій pthreads потрібна mutex?


182

Я читаю далі pthread.h; функції, пов'язані зі змінною умови (як pthread_cond_wait(3)), потребують mutex як аргумент. Чому? Наскільки я можу сказати, я буду створювати мютекс просто для використання в якості аргументу? Що це мутекс?

Відповіді:


194

Це лише те, як змінені умови (або були спочатку) реалізовані.

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

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

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

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 мала гідну реалізацію :-)

У будь-якому випадку той самий код, який обробляв помилковий випадок, також обробляв справжні помилкові пробудження, оскільки прапор, доступний для роботи, не буде встановлений для них.


3
'робити щось' не повинно знаходитися всередині циклу while. Ви хочете, щоб ваш цикл while просто перевірив стан, інакше ви також можете "зробити щось", якщо отримаєте помилкове пробудження.
Н.З.К.

1
ні, обробка помилок є другою для цього. За допомогою pthreads ви можете прокинутися без видимих ​​причин (помилкове пробудження) і з будь-якою помилкою. Таким чином, вам потрібно повторно перевірити "деяку умову" після пробудження.
ніс

1
Я не впевнений, що розумію. Я мав таку ж реакцію, що і нос ; чому do somethingвсередині whileпетлі?
ELLIOTTCABLE

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

7
@stefaanv "мютекс все ще захищає змінну умови, іншого способу її захистити немає": мютекс не захищає змінну умови; це захист даних предикатів , але я думаю, ви знаєте, що з читання вашого коментаря, який слідував за цим твердженням. Ви можете сигналізувати змінної умови законно і повністю підтримується реалізаціями, після -unlock мьютекса обгорткового предикат, а насправді ви будете зменшити конкуренцію в цьому в деяких випадках.
WhozCraig

59

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

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)бути цикл виконання часу, щоб він чекав змінної умови хоча б раз?
Суддя Мейґарден

3
Ні. Те, що ви насправді чекаєте, - це те, що "some_data" не має значення. Якщо це "перший раз" не має значення, чудово, ви зберігаєте мютекс і можете безпечно використовувати дані. Якщо у вас був цикл do / while, ви пропустите сповіщення, якщо хтось подав сигнал про змінну стану, перш ніж ви його чекали (це не що інше, як події, виявлені на win32, які залишаються сигналами, поки хтось їх не чекає)
нос

4
Я просто натрапив на це питання і, відверто кажучи, дивно виявити, що ця відповідь, яка є правильною, має набагато менше балів, ніж відповідь paxdiablo, яка має певні вади (атомність все-таки потрібна, мютекс потрібен лише для обробки стану, не для обробки чи повідомлення). Я здогадуюсь, що саме так працює
stackoverflow

@stefaanv, якщо ви хочете деталізувати недоліки, як коментарі до моєї відповіді, щоб я їх вчасно бачив, а не через місяці пізніше :-), я буду радий їх виправити. Ваші короткі фрази насправді не дають мені достатньо деталей, щоб розробити те, що ви намагаєтесь сказати.
paxdiablo

1
@nos, не повинно while(some_data != NULL)бути while(some_data == NULL)?
Eric Z

30

Змінні умови 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. Зауважте, що сама змінна умови не має поняття, є робота чи ні. Тобто змінна умова повинна бути пов’язана з умовою, цей стан повинен підтримуватися вашим кодом, а оскільки він поділяється між потоками, він повинен бути захищений мютекс.


1
Або, коротше кажучи, вся змінна умова умови полягає в тому, щоб забезпечити атомну операцію "розблокувати і чекати". Без мютексу не було б чого розблокувати.
Девід Шварц

Ви б не проти пояснити значення без громадянства ?
ЗСШ

@snr У них немає держави. Вони не "заблоковані", "сигналізовані" або "несигналізовані". Отже, ви несете відповідальність за відстеження будь-якого стану, пов'язаного зі змінною умови. Наприклад, якщо змінна умови повідомляє потоку, коли черга стає не порожньою, має бути випадок, коли один потік може зробити чергу не порожньою, а якийсь інший потік повинен знати, коли черга стає не порожньою. Це загальний стан, і ви повинні захистити його мютексним файлом. Ви можете використовувати змінну умови в поєднанні із спільним станом, захищеним мютекс, як механізм пробудження.
Девід Шварц

16

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

Операції очікування об'єднують змінну стану та мютекс, оскільки:

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

З цієї причини операція очікування приймає аргументи як mutex, так і умову: так, щоб вона могла керувати атомним переносом потоку від володіння мютекс до очікування, щоб нитка не стала жертвою втраченого стану гонки пробудження .

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

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


Чи можете ви надати посилання на те, що операції трансляції не потребують придбання мютексу? У MSVC мовлення ігнорується.
xvan

@xvan POSIX pthread_cond_broadcastта pthread_cond_signalоперації (про які йдеться у цьому питанні про SO) навіть не сприймають мютекс як аргумент; тільки умова. Специфікація POSIX тут . Про мютекс згадується лише посиланням на те, що відбувається в нитках очікування, коли вони прокидаються.
Каз

Ви б не проти пояснити значення без громадянства ?
ЗСШ

1
@snr Об'єкт синхронізації без стану не пам’ятає жодного стану, пов’язаного із сигналізацією. За сигналом, якщо щось на нього зараз чекає, воно прокидається, інакше пробудження забудеться. Змінні умови стану без стану. Необхідний стан, щоб зробити синхронізацію надійною, підтримується додатком і захищається мютекс, який використовується спільно зі змінними умовами, відповідно до правильно записаної логіки.
Каз

7

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

mutex.lock()
while(!check())
    condition.wait()
mutex.unlock()

Існує три причини обгортання wait()мутексу:

  1. без мютексу ще одна нитка могла б signal()до цього, wait()і ми пропустимо це пробудження.
  2. зазвичай check()залежить від модифікації з іншого потоку, тому вам все одно потрібно взаємне виключення.
  3. щоб переконатися, що потік найвищого пріоритету розпочинається першим (черга для mutex дозволяє планувальнику вирішити, хто йде далі).

Третій момент не завжди викликає занепокоєння - історичний контекст пов'язаний із статті до цієї розмови .

Щодо цього механізму часто згадуються помилкові пробудження (тобто нитка очікування прокидається без виклику signal()). Однак такі події обробляються петлею check().


4

Змінні умови пов'язані з мютекс, оскільки це єдиний спосіб уникнути гонки, якої він призначений уникати.

// 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);

3

Мютекс повинен бути заблокований під час дзвінка pthread_cond_wait; коли ви називаєте це атомарно, обидва розблокує мютекс, а потім блокує умову. Після того, як умова буде сигналізована, вона атомно блокує її знову і повертається.

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


Отже… чи є в мене причина не просто залишити мютекс завжди розблокованим, а потім заблокувати його перед тим, як чекати, а потім розблокувати його відразу після закінчення очікування?
ELLIOTTCABLE

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

Отже ... я спершу слід зачекати на мютекс стануватора, перш ніж чекати на умовному стані? Я не впевнений, що взагалі розумію.
ELLIOTTCABLE

2
@elliottcable: Не тримаючи мютекс, як ви могли знати, варто чи не варто чекати? Що робити, якщо те, що ви чекаєте, щойно сталося?
Девід Шварц

1

Я зробив напругу в класі, якщо ви хочете реальний приклад змінної умови:

#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);
}

1

Це представляється конкретним дизайнерським рішенням, а не концептуальною потребою.

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

https://linux.die.net/man/3/pthread_cond_wait

Особливості Mutexes та змінних умов

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


0

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

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і сигнали, але ні один потік не чекає , і , таким чином , ні один потік не прокинувся. Коли батько знову біжить, він вічно спить, що насправді неприємно.

Що робити, якщо вони виконуються під час придбання замків індивідуально?

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