停止长时间休眠的线程

32

假设我有一个线程,应该定期执行某个任务,但这个周期是每小时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()函数,在start()之后1毫秒,线程将在等待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
每小时6次(每5分钟一次),那么是每小时12次还是每10分钟一次呢? :) - Alexandre Lavoie
@AlexandreLavoie 真是个失败啊!谢谢,我会纠正的! :) - PaperBirdMaster
3
与固定睡眠时间不同,您可以进入可信号唤醒的等待状态来等待一段时间,以便其他线程仍然可以中断您。原文链接:http://en.cppreference.com/w/cpp/thread/condition_variable,参见其中的第一句话。 - stijn
另一个选择是boost::basic_waitable_timer - tenfour
你的线程应该每5分钟做一些工作,还是在5分钟后第一次完成工作,然后再睡5分钟?如果是前者,那么我会(在Windows上)创建一个定时器,每5分钟创建一个执行一些工作的线程。如果创建线程开销太大,我会使用线程池。 - user743414
3个回答

37

使用条件变量。您可以等待条件变量 5分钟的过去。请记得检查虚假唤醒。

cppreference

我在谷歌搜索了一两分钟,没有找到如何使用条件变量的好的stack overflow帖子。棘手的部分是要意识到wait可能会在5分钟内没有过去,也没有收到信号的情况下就被唤醒。处理这个问题最干净的方式是使用 lambda 表达式来双重检查唤醒是否“有效”。

此处是一些在 cppreference 上使用带有 lambda 的 wait_until 的示例代码。(带有 lambda 的 wait_for 等效于带有 lambda 的 wait_until)。我稍微修改了它。

以下是版本:

struct timer_killer {
  // returns false if killed:
  template<class R, class P>
  bool wait_for( std::chrono::duration<R,P> const& time ) const {
    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; // should be modified inside mutex lock
    cv.notify_all(); // it is safe, and *sometimes* optimal, to do this outside the lock
  }
  // I like to explicitly delete/default special member functions:
  timer_killer() = default;
  timer_killer(timer_killer&&)=delete;
  timer_killer(timer_killer const&)=delete;
  timer_killer& operator=(timer_killer&&)=delete;
  timer_killer& operator=(timer_killer const&)=delete;
private:
  mutable std::condition_variable cv;
  mutable std::mutex m;
  bool terminate = false;
};

现成的例子

您在共享位置创建一个timer_killer。客户端线程可以使用wait_for(时间)。如果返回false,则表示您在等待完成之前被杀死了。

控制线程只需调用kill(),正在执行wait_for的所有线程都会得到false的返回值。

请注意,这里存在一些争用(锁定互斥量),因此不适合无限线程(但几乎没有什么东西适合)。如果您需要具有任意延迟的未绑定任务的无限数量而不是每个延迟重复任务的完整线程,请考虑使用调度程序 - 每个真实线程使用的系统地址空间至少为一个兆字节(仅用于堆栈)。


在 kill 方法中,应该将 'std::unique_lockstd::mutex' 替换为 'std::lock_guardstd::mutex' 吗? - user1633272
如果你不调用lock.lock(),它就什么也不做。 - user1633272
@user1633272 不是的,unique_lock 只是一个可移动的锁卫。你可以使用互斥量创建一个未锁定的 unique_lock,但这不是它们默认创建的方式。 - Yakk - Adam Nevraumont
1
@kaip 已修复,同时修改了 kill 函数以考虑 cv 的现代编译器优化。 - Yakk - Adam Nevraumont
如果像你在答案中指示的那样,调用cv.notify_all()是安全的,为什么不将kill()中的前两行代码放在内部作用域中,以便在cv.notify_all()之前解锁锁定? - SubMachine
显示剩余6条评论

5

有两种传统的方法可以做到这一点。

您可以在条件变量上设置定时等待,让另一个线程发信号给您的周期线程,在到达指定时间时唤醒并退出。

或者,您可以使用 poll 在管道上进行轮询,并将睡眠作为超时。然后,您只需向管道写入一个字节,线程就会被唤醒并退出。


-3

是的,通过使用 std::mutexstd::lock_guardstd::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
  }
}

3
无法编译。 - Barry

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接