Второй вариант — заставить ожидающий поток спать между проверками с помощью функции std::this_thread::sleep_for()
(см. раздел 4.3):
bool flag;
std::mutex m;
void wait_for_flag() {
std::unique_lock<std::mutex> lk(m); ←
(1) Освободить мьютекс
while (!flag) {
lk.unlock(); ←
(2) Спать 100 мс
std::this_thread::sleep_for(std::chrono::milliseconds(100));
lk.lock(); ←
(3) Снова захватить мьютекс
}
}
В этом цикле функция освобождает мьютекс (1) перед тем, как заснуть (2), и снова захватывает его, проснувшись, (3), оставляя другому потоку шанс захватить мьютекс и поднять флаг.
Это уже лучше, потому что во время сна поток не расходует процессорное время. Но трудно выбрать подходящий промежуток времени. Если он слишком короткий, то поток все равно впустую тратит время на проверку; если слишком длинный — то поток будет спать и после того, как ожидание завершилось, то есть появляется ненужная задержка. Редко бывает так, что слишком длительный сон прямо влияет на работу программу, но в динамичной игре это может привести к пропуску кадров, а в приложении реального времени — к исчерпанию выделенного временного кванта.
Третий — и наиболее предпочтительный - способ состоит в том, чтобы воспользоваться средствами из стандартной библиотеки С++, которые позволяют потоку ждать события. Самый простой механизм ожидания события, возникающего в другом потоке (например, появления нового задания в упоминавшемся выше конвейере), дают условные переменные. Концептуально условная переменная ассоциирована с каким-то событием или иным условием, причём один или несколько потоков могут ждать, когда это условие окажется выполненным. Если некоторый поток решит, что условие выполнено, он может известить об этом один или несколько потоков, ожидающих условную переменную, в результате чего они возобновят работу.
4.1.1. Ожидание условия с помощью условных переменных
Стандартная библиотека С++ предоставляет не одну, а две реализации условных переменных: std::condition_variable
и std::condition_variable_any
. Оба класса объявлены в заголовке <condition_variable>
. В обоих случаях для обеспечения синхронизации необходимо взаимодействие с мьютексом; первый класс может работать только с std::mutex
, второй — с любым классом, который отвечает минимальным требованиям к «мьютексоподобию», отсюда и суффикс _any
. Поскольку класс std::condition_variable_any
более общий, то его использование может обойтись дороже с точки зрения объема потребляемой памяти, производительности и ресурсов операционной системы. Поэтому, если дополнительная гибкость не требуется, то лучше ограничиться классом std::condition_variable
.
Ну и как же воспользоваться классом std::condition_variable
в примере, упомянутом во введении, — как сделать, чтобы поток, ожидающий работу, спал, пока не поступят данные? В следующем листинге приведён пример реализации с использованием условной переменной.
Листинг 4.1. Ожидание данных с помощью std::condition_variable
std::mutex mut;
std::queue<data_chunk> data_queue; ←
(1)
std::condition_variable data_cond;
void data_preparation_thread() {
while (more_data_to_prepare()) {
data_chunk const data = prepare_data();
std::lock_guard<std::mutex> lk(mut);