Остановка длинноволновых потоков

Предположим, что у меня есть поток, который должен выполнять некоторую задачу периодически, но этот период 6 раз в час 12 раз в час (каждые 5 минут), я часто видел код, который контролирует поток цикл с флажком is_running, который проверяется каждый цикл, например:

std::atomic<bool> is_running;

void start()
{
    is_running.store(true);
    std::thread { thread_function }.detach();
}

void stop()
{
    is_running.store(false);
}

void thread_function()
{
    using namespace std::literals;
    while (is_running.load())
    {
        // do some task...
        std::this_thread::sleep_for(5min);
    }
}

Но если вызывается функция stop(), скажем, 1 миллисекунда после start(), поток будет живым для 299999 дополнительных миллисекунд, пока он не проснется, не проверит флаг и не умрет.

Правильно ли я понимаю? Как избежать сохранения (но спящего) потока, который должен был быть закончен? Мой лучший подход до сих пор следующий:

void thread_function()
{
    using namespace std::literals;
    while (is_running.load())
    {
        // do some task...
        for (unsigned int b = 0u, e = 1500u; is_running.load() && (b != e); ++b)
        {
            // 1500 * 200 = 300000ms = 5min
            std::this_thread::sleep_for(200ms);
        }
    }
}

Есть ли более грязный и более простой способ достичь этого?

Ответ 1

Используйте переменную условия. Вы ждете переменную условия или 5 минут. Не забудьте проверить наличие побочных пробуждений.

cppreference

Я не могу найти хороший пост о том, как использовать переменную условия через минуту или два поиска в Google. Трудная часть понимает, что wait может просыпаться с проходом не 5 минут, ни отсылаемого сигнала. Самый чистый способ справиться с этим - использовать методы wait с лямбдой, которая проверяет, что пробуждение было "хорошим".

здесь - это некоторый пример кода в cppreference, который использует wait_until с lambda. (wait_for с лямбдой эквивалентно wait_until с lambda). Я полагаю, что это хорошо.

Вот версия:

struct timer_killer {
  // returns false if killed:
  template<class R, class P>
  bool wait_for( std::chrono::duration<R,P> const& time ) {
    std::unique_lock<std::mutex> lock(m);
    return !cv.wait_for(lock, time, [&]{return terminate;});
  }
  void kill() {
    std::unique_lock<std::mutex> lock(m);
    terminate=true;
    cv.notify_all();
  }
private:
  std::condition_variable cv;
  std::mutex m;
  bool terminate = false;
};

живой пример.

Вы создаете timer_killer в общем месте. Потоки клиента могут wait_for( time ). Если он возвращает false, это означает, что вы были убиты до того, как ваше ожидание закончилось.

Управляющий поток просто вызывает kill(), и все, делающие wait_for, получают false return.

Обратите внимание, что есть некоторая разногласия (блокировка мьютекса), поэтому это не подходит для бесконечных потоков (но мало что есть). Подумайте об использовании планировщика, если вам нужно иметь неограниченное количество задач, которые запускаются с произвольными задержками, а не полный поток за каждую задержанную повторяющуюся задачу - каждый реальный поток превышает мегабайт пространства адресной системы (только для стека).

Ответ 2

Есть два традиционных способа сделать это.

Вы можете использовать временное ожидание переменной условия, а другой поток сигнализировать о том, что ваша периодическая нить пробуждается и умирает, когда это время.

В качестве альтернативы вы могли бы poll на трубе с вашим сном в качестве таймаута вместо сна. Затем вы просто записываете байт в трубу, и поток просыпается и может выйти.

Ответ 3

да, с помощью std::mutex, std::lock_guard и std::conditional_variable:

std::mutex mtx;
std::conditional_variable cv;

void someThreadFunction (){
   while(!stopThreadFlag){
     std::lock_gurad<std::mutex> lg(mtx);
     cv.wait_for(lg,SOME_ITERVAL,!stopThreadFlag || wakeTheThreadVariable);
     //the rest here
  }
}