mirror of
https://github.com/Nekrolm/ubbook.git
synced 2025-12-17 21:04:35 +03:00
107 lines
7.6 KiB
Markdown
107 lines
7.6 KiB
Markdown
# Condition variable, или как сделать все правильно и уйти в deadlock
|
||
|
||
Синхронизация потоков это сложно, хотя у нас и есть примитивы. Такой себе каламбур.
|
||
Хорошо, если есть готовые высокоуровневые абстракции в виде очередей или каналов.
|
||
Но иногда приходится мастерить их самому. С использованием более низкоуровневых вещей: мьютексов, атомарных переменных и обвязки вокруг них.
|
||
|
||
`condition_variable` — примитив синхронизации, позволяющий одному или нескольким потокам ожидать сообщений от других потоков. Ожидать пассивно, не тратя время CPU впустую на постоянные проверки чего-то в цикле. Поток просто снимается с исполнения, ставится в очередь операционной системой, а по наступлении определенного события — уведомления — от другого потока пробуждается. Все замечательно и удобно.
|
||
|
||
Сам по себе примитив `condition_variable` не передает никакой информации, а только служит для пробуждения или усыпления потоков. Причем пробуждения, из-за особенностей реализации блокировок, могут случаться ложно, самопроизвольно (spurious), а не только лишь по непосредственной команде через `condition_variable`.
|
||
|
||
Потому типичное использование требует дополнительной проверки условий и выглядит как-то так.
|
||
|
||
```C++
|
||
std::condition_variable cv;
|
||
std::mutex event_mutex;
|
||
bool event_happened = false;
|
||
|
||
// исполняется в одном потоке
|
||
void task1() {
|
||
std::unique_lock lock { event_mutex };
|
||
// предикат гарантированно проверяется под захваченной блокировкой
|
||
cv.wait(lock, [&] { return event_happened; }); // безпредикатная версия wait ждет только уведомления,
|
||
// но может произойти ложное пробуждения (обычно, если кто-то отпускает этот же мьютекс)
|
||
...
|
||
// дождались — событие наступило
|
||
// выполняем что нужно
|
||
}
|
||
|
||
// исполняется в другом потоке
|
||
void task2() {
|
||
...
|
||
{
|
||
std::lock_guard lock {event_mutex};
|
||
event_happened = true;
|
||
}
|
||
// Обратите внимание: вызов notify не обязан быть под захваченной блокировкой.
|
||
// Однако, в ранних версиях msvc, а также в очень старой версии из boost были
|
||
// баги, требующие удерживать мьютекс захваченным во время вызова notify()
|
||
// Но есть случай, когда делать вызов notify под блокировкой необходимо — если
|
||
// другой тред может вызвать, например, завершаясь, деструктор объекта cv
|
||
cv.notify_one(); // notify_all()
|
||
}
|
||
```
|
||
|
||
Хм, внимательный читатель может сообразить, что в task2 мьютекс используется только для защиты булевого флага.
|
||
Невиданное расточительство! Целых два системных вызова в худшем случае. Давайте лучше флаг сделаем атомарным!
|
||
|
||
```C++
|
||
std::atomic_bool event_happened = false;
|
||
std::condition_variable cv;
|
||
std::mutex event_mutex;
|
||
|
||
void task1() {
|
||
std::unique_lock lock { event_mutex };
|
||
cv.wait(lock, [&] { return event_happened; });
|
||
...
|
||
}
|
||
|
||
void task2() {
|
||
...
|
||
event_happened = true;
|
||
cv.notify_one(); // notify_all()
|
||
...
|
||
}
|
||
```
|
||
|
||
Компилируем, запускаем, работает — классно, срочно в релиз!
|
||
|
||
Но однажды приходит пользователь и говорит, что запустил task1 и task2 как обычно одновременно, но сегодня внезапно task1 не завершается, хотя task2 отработал! Вы идете к пользователю, смотрите — висит. Перезапускаете — не зависает. Еще перезапускаете — опять не зависает. Перезапускаете 50 раз — все равно не зависает. Сбой какой-то железный разовый, думаете вы.
|
||
|
||
Уходите. Через месяц пользователь опять приходит с той же проблемой. И опять не воспроизводится. Ну точно железный сбой, космическая радиация битик какой-то в локальном кэше треда выбивает. Ничего страшного...
|
||
|
||
На самом деле в программе ошибка, приводящая к блокировке при редком совпадении в порядке инструкций. Чтобы понять ее, нужно посмотреть внимательнее на то, как устроен метод `wait` с предикатом.
|
||
|
||
```C++
|
||
// thread a
|
||
std::unique_lock lock {event_mutex}; // a1
|
||
// cv.wait(lock, [&] { return event_happened; }) это
|
||
while (![&] { return event_happened; }()) { // a2
|
||
cv.wait(lock); // a3
|
||
}
|
||
|
||
// -------------------------------
|
||
// thread b
|
||
event_happened = true; // b1
|
||
cv.notify_one(); // b2
|
||
```
|
||
|
||
Рассмотрим следующую последовательность исполнения строчек в двух потоках
|
||
|
||
```
|
||
a1 // поток 1 захватывает блокировку
|
||
a2 // поток 1 проверяет условие --- истинно, событие не наступило
|
||
b1 // поток 2 выставляет событие,
|
||
b2 // уведомляет о нем, но поток 1 еще не начал ждать! уведомление потеряно!
|
||
a3 // поток 1 начинает ждать и никогда не дождется!
|
||
```
|
||
Отоптимизировали? Возвращайте захват мьютекса обратно.
|
||
|
||
Захват мьютекса в уведомляющем потоке гарантирует, что ожидающий уведомления поток либо еще не начал ждать и проверять событие, либо уже ждет. Если же мьютекс не захвачен, мы можем попасть в промежуточное состояние.
|
||
|
||
Осторожнее с примитивами!
|
||
|
||
## Полезные ссылки
|
||
1. https://en.cppreference.com/w/cpp/thread/condition_variable
|
||
2. https://stackoverflow.com/questions/2531359/do-condition-variables-still-need-a-mutex-if-youre-changing-the-checked-value-a/2531397#2531397
|
||
3. https://www.modernescpp.com/index.php/c-core-guidelines-be-aware-of-the-traps-of-condition-variables |