Мне тут потребовался примитив синхронизации, в чем-то похожий на добавленный в C++20 std::latch. Но с важным отличием: в `std::latch` нужно в конструкторе указывать значение счетчика. А в моем случае это количество заранее точно неизвестно.
Грубо говоря, сценарий использования `std::latch`: есть тред A, который ждет, пока N тредов B(i) сделают кусок работы. Тред A засыпает на `wait`, каждый тред B(i) рано или поздно вызывает `count_down` и когда это сделают все треды B(i), тред А проснется.
Все это отлично работает пока N известно заранее.
В моем же случае тред С создает сперва тред A, а затем начинает создавать треды B. И тред A точно не знает, сколько именно C создаст тредов B. Просто в какой-то момент треду A нужно будет дождаться пока запущенные треды B завершат свою работу. Для чего каждый тред B сперва инкрементирует счетчик, а затем декрементирует. Треду же А достаточно дождаться обнуления этого счетчика.
Сделанный для этих целей простой вариант "барьера" можно увидеть под катом.
Используется приблизительно следующим образом:
// Это все внутри треда C. meeting_room_t completion_room; std::thread thread_a{[&]() { ... // что-то делает. // Нужно дождаться пока треды B завершат свою работу. completion_room.wait_then_close(); ... // еще что-то делает. }}; // Создаем треды B. std::vector<std::thread> threads_b; while(some_condition()) { threads_b.push_back([&completion_room]() { completion_room.enter(); // Увеличили счетчик. ... // что-то делает. completion_room.leave(); // Уменьшили счетчик. }); ... // какие-то еще действия. } // Осталось дождаться завершения работы. for(auto & t : threads_b) t.join(); thread_a.join(); |
Возникла сложность с названием для такого примитива синхронизации.
Пока что у меня есть такая аналогия: комната для совещаний в офисе. В течении рабочего дня ее могут занимать для проведения совещаний, а в конце рабочего дня ее нужно закрыть. Но закрывать можно только когда в ней никого нет. Если же в комнате совещание идет, то участники могут туда заходить и выходить оттуда: пока там есть хотя бы один человек, то совещание считается незавершенным и закрывать комнату нельзя. Когда же все участники совещания комнату покинули, то считается, что совещание закончено и комнату можно закрыть.
Поэтому пока в качестве рабочего названия используется meeting_room. Однако, есть ощущение, что название не самое удачное. Вот и пытаюсь воспользоваться чужой помощью, чтобы придумать что-то получше.
Upd. Похоже, что такая штука назвается rundown: Run-Down Protection. Большое спасибо Константину за наводку.
Текущая реализация meeting_room_t:
class meeting_room_t { std::mutex lock_; std::condition_variable wakeup_cv_; bool closed_{false}; unsigned attenders_{}; public: meeting_room_t() = default; void enter() { std::lock_guard<std::mutex> lock{lock_}; if(closed_) throw std::runtime_error{"meeting_room is closed"}; ++attenders_; } void leave() noexcept { std::lock_guard<std::mutex> lock{lock_}; --attenders_; if(!attenders_) wakeup_cv_.notify_all(); } void wait_then_close() { std::unique_lock<std::mutex> lock{lock_}; if(attenders_) { wakeup_cv_.wait(lock, [this]{ return 0u == attenders_; }); closed_ = true; } } }; |
6 комментариев:
Growable latch или dynamic latch, т.к. семантика с latch очень схожая.
> Growable latch или dynamic latch, т.к. семантика с latch очень схожая.
dynamic latch -- это интересно, спасибо!
На всякий случай спрошу - а как застраховаться от ситуации, когда пару потоков увеличат счётчик и успеют уменьшить его до нуля, опередив запуск оставшихся потоков B? По идее счётчик, не зная о том, сколько должно быть потоков, словит событие обнуления и проигнорирует работу не успевших B, так?
Насчёт названия - поскольку упоминалось подобие cyclic barrier, лично мне был бы понятен вариант типа arbitrary_barrieric_latch, либо, обращая внимание на сделанный упор хотя бы одного ожидаемого потока, any_alive_waiting_latch - по названию понятна его цель.
Ещё я бы предложил infinite_semaphoric_latch_singleshot, ведь у него принцип похож на семафор в том смысле, что когда доходит до нуля, работа останавливается.
> По идее счётчик, не зная о том, сколько должно быть потоков, словит событие обнуления и проигнорирует работу не успевших B, так?
Да, проигнорирует. Но тут работает принцип "кто не успел, тот опоздал". Если какой-то из B не успел к дедлайну, то результат его работы уже не нужен.
Отправить комментарий