суббота, 10 февраля 2024 г.

[prog.multithreading] Нужна помощь в поиске названия для примитива синхронизации, похожего на std::latch

Мне тут потребовался примитив синхронизации, в чем-то похожий на добавленный в 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 комментариев:

Dmitry Igrishin комментирует...

Growable latch или dynamic latch, т.к. семантика с latch очень схожая.

eao197 комментирует...

> Growable latch или dynamic latch, т.к. семантика с latch очень схожая.

dynamic latch -- это интересно, спасибо!

АлСт комментирует...

На всякий случай спрошу - а как застраховаться от ситуации, когда пару потоков увеличат счётчик и успеют уменьшить его до нуля, опередив запуск оставшихся потоков B? По идее счётчик, не зная о том, сколько должно быть потоков, словит событие обнуления и проигнорирует работу не успевших B, так?

АлСт комментирует...

Насчёт названия - поскольку упоминалось подобие cyclic barrier, лично мне был бы понятен вариант типа arbitrary_barrieric_latch, либо, обращая внимание на сделанный упор хотя бы одного ожидаемого потока, any_alive_waiting_latch - по названию понятна его цель.

АлСт комментирует...

Ещё я бы предложил infinite_semaphoric_latch_singleshot, ведь у него принцип похож на семафор в том смысле, что когда доходит до нуля, работа останавливается.

eao197 комментирует...

> По идее счётчик, не зная о том, сколько должно быть потоков, словит событие обнуления и проигнорирует работу не успевших B, так?

Да, проигнорирует. Но тут работает принцип "кто не успел, тот опоздал". Если какой-то из B не успел к дедлайну, то результат его работы уже не нужен.