Просто додавши цю відповідь, оскільки я вважаю, що прийнята відповідь може ввести в оману. У всіх випадках вам потрібно буде заблокувати мьютекс, перш ніж десь викликати notify_one (), щоб ваш код був потокобезпечним, хоча ви можете розблокувати його ще раз перед тим, як фактично викликати notify _ * ().
Для уточнення, ви ПОВИННІ взяти замок перед тим, як ввести wait (lk), оскільки wait () розблоковує lk, і це буде невизначеною поведінкою, якщо замок не буде заблокований. Це не так з notify_one (), але вам потрібно переконатися, що ви не будете викликати notify _ * (), перш ніж вводити функцію wait () і розблокувати мьютекс; що, очевидно, можна зробити лише заблокувавши той самий мьютекс перед тим, як зателефонувати до сповіщення _ * ().
Наприклад, розглянемо такий випадок:
std::atomic_int count;
std::mutex cancel_mutex;
std::condition_variable cancel_cv;
void stop()
{
if (count.fetch_sub(1) == -999)
cv.notify_one();
}
bool start()
{
if (count.fetch_add(1) >= 0)
return true;
stop();
return false;
}
void cancel()
{
if (count.fetch_sub(1000) == 0)
return;
std::unique_lock<std::mutex> lk(cancel_mutex);
cancel_cv.wait(lk);
}
Попередження : цей код містить помилку.
Ідея полягає в наступному: потоки викликають start () і stop () попарно, але лише до тих пір, поки start () повертає true. Наприклад:
if (start())
{
stop();
}
Один (інший) потік у певний момент викличе cancel (), а після повернення з cancel () знищить об’єкти, які потрібні в пункті «Здійснювати речі». Однак, cancel () не повинен повертатися, поки між start () і stop () є потоки, а після того, як cancel () виконає свій перший рядок, start () завжди поверне false, тому жодні нові потоки не будуть входити в 'Do зона 'речі'.
Працює правильно?
Міркування такі:
1) Якщо будь-який потік успішно виконує перший рядок start () (і, отже, поверне true), тоді жоден потік ще не виконав перший рядок cancel () (ми вважаємо, що загальна кількість потоків набагато менше 1000 на спосіб).
2) Крім того, хоча потік успішно виконав перший рядок start (), але ще не перший рядок stop (), тоді неможливо, щоб будь-який потік успішно виконав перший рядок cancel () (зверніть увагу, що лише один потік коли-небудь викликати cancel ()): значення, яке повертає fetch_sub (1000), буде більшим за 0.
3) Як тільки потік виконує перший рядок cancel (), перший рядок start () завжди повертає false, а потік, що викликає start (), більше не буде входити в область 'Do stuff'.
4) Кількість дзвінків для запуску () та зупинки () завжди збалансовано, тому після того, як перший рядок cancel () буде невдало виконаний, завжди буде момент, коли (останній) дзвінок для зупинки () викликає підрахунок досягти -1000 і, отже, notify_one () для виклику. Зауважте, що це може статися лише тоді, коли перший рядок скасування призвів до того, що ця нитка пропала.
Окрім проблеми голоду, коли стільки потоків викликає start () / stop (), що кількість ніколи не досягає -1000, а cancel () ніколи не повертається, що можна прийняти як "малоймовірне і ніколи не триває довго", є ще одна помилка:
Цілком можливо, що в області «Здійснити» є одна нитка, скажімо, це просто виклик stop (); в цей момент потік виконує перший рядок cancel (), зчитуючи значення 1 за допомогою fetch_sub (1000) і потрапляючи. Але перед тим, як взяти мьютекс і / або зробити виклик для очікування (lk), перший потік виконує перший рядок stop (), читає -999 і викликає cv.notify_one ()!
Тоді цей виклик notify_one () виконується ДО того, як ми зачекаємо () - внесення змінної умови! І програма буде на невизначений час безвихідна.
З цієї причини ми не можемо викликати notify_one (), поки ми не зателефонуємо wait (). Зверніть увагу, що потужність змінної умови полягає в тому, що вона здатна атомно розблокувати мьютекс, перевірити, чи відбувся виклик notify_one () і перейти в режим сну чи ні. Ви не можете обдурити його, але робити треба тримати м'ютекс заблокований щоразу , коли ви вносите зміни в змінних , які можуть змінити стан від помилкового до істини і тримати його під замком при виклику notify_one () з - за умов гонки , як описано тут.
У цьому прикладі немає жодної умови. Чому я не використав як умову 'count == -1000'? Оскільки тут це зовсім не цікаво: як тільки взагалі буде досягнуто значення -1000, ми впевнені, що жоден новий потік не потрапить у область "Здійснювати речі". Більше того, потоки все ще можуть викликати start () і збільшуватимуть кількість (до -999 та -998 тощо), але нас це не хвилює. Важливо лише те, що було досягнуто -1000 - щоб ми точно знали, що в області "Здійснювати речі" більше немає потоків. Ми впевнені, що це так, коли викликається notify_one (), але як переконатися, що ми не викликаємо notify_one () до того, як cancel () заблокував його мьютекс? Просто блокування cancel_mutex незадовго до notify_one () не допоможе, звичайно.
Проблема полягає в тому, що, незважаючи на те, що ми не чекаємо умови, умова все ще є , і нам потрібно заблокувати мьютекс
1) до досягнення цієї умови 2) перед тим, як ми зателефонуємо до notify_one.
Тому правильним кодом стає:
void stop()
{
if (count.fetch_sub(1) == -999)
{
cancel_mutex.lock();
cancel_mutex.unlock();
cv.notify_one();
}
}
[... той самий старт () ...]
void cancel()
{
std::unique_lock<std::mutex> lk(cancel_mutex);
if (count.fetch_sub(1000) == 0)
return;
cancel_cv.wait(lk);
}
Звичайно, це лише один приклад, але інші випадки дуже схожі; майже у всіх випадках, коли ви використовуєте умовну змінну, вам потрібно буде заблокувати цей мьютекс (незабаром) перед викликом notify_one (), інакше можливо, що ви викликаєте його перед викликом wait ().
Зверніть увагу, що я розблокував мьютекс перед викликом notify_one (), оскільки в іншому випадку існує (мала) ймовірність того, що виклик notify_one () пробудить потік, чекаючи змінної умови, яка потім спробує взяти мьютекс і блок, перш ніж ми знову звільнимо мьютекс. Це лише трохи повільніше, ніж потрібно.
Цей приклад був якось особливим у тому, що рядок, що змінює умову, виконується тим самим потоком, який викликає wait ().
Більш звичним є випадок, коли один потік просто чекає, поки умова стане істинним, а інший потік бере блокування перед зміною змінних, що беруть участь у цій умові (змушуючи її стати істинною). В цьому випадку м'ютекс буде негайно заблокований до того (і після) умови збулося - так це абсолютно нормально , щоб просто розблокувати м'ютекс перед викликом повідомить _ * () в цьому випадку.
wait morphing
оптимізацію) Основне правило, пояснене за цим посиланням: сповіщати блокування краще в ситуаціях із більш ніж 2 потоками для отримання більш передбачуваних результатів.