停止长时间睡眠线程

问题描述 投票:0回答:2

假设我有一个线程应该定期执行某些任务,但是这个周期是每小时 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);
        }
    }
}

有没有一种更简单、更直接的方法来实现这一目标?

c++ multithreading c++11 thread-sleep
2个回答
39
投票

使用条件变量。 您等待条件变量 or 5 分钟过去。 请记住检查虚假唤醒。

cpp参考

我在谷歌搜索一两分钟内找不到关于如何使用条件变量的好的堆栈溢出帖子。 棘手的部分是意识到

wait
无法在 5 分钟过去或发送信号的情况下醒来。 处理此问题的最简洁方法是使用带有 lambda 的等待方法,该 lambda 会双重检查唤醒是否“良好”。

here 是 cppreference 上的一些示例代码,它使用

wait_until
和 lambda。 (带有 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( time )
。 如果它返回 false,则意味着您在等待完成之前就被杀了。

控制线程只需调用

kill()
,每个执行
wait_for
操作的人都会获得
false
返回。

请注意,存在一些争用(互斥锁的锁定),因此这不适合无限线程(但很少有情况适合)。 如果您需要以任意延迟运行无限数量的任务,而不是每个延迟重复任务运行一个完整的线程,请考虑使用调度程序 - 每个实际线程都使用超过一兆字节的系统地址空间(仅用于堆栈) .


timer_killer
写得不好。 您应该将业务逻辑移出低级线程细节,例如:

enum class notify {
  none,
  one,
  all,
};
 
template<class T>
struct waitable {
  // Returns true when f(t) returns true, false on timeout:
  template<class F, class R, class P> 
  bool wait_for(std::chrono::duration<R,P> const& time, F f) const {
    auto l = lock();
    return cv.wait_for(l, time, f(t));
  }
  template<class F>
  auto read( F f ) const {
    auto l = lock();
    return f(t);
  }
  template<class F>
  void modify(F f) {
    auto l = lock();
    notify n = f(t);
    switch(n) {
      case notify::none: return;
      case notify::all: cv.notify_all(); return;
      case notify::one: cv.notify_one(); return;
    }
  }
  waitable( T in ): t(std::forward<T>(in)) {}
  
private:
  auto lock() const {
    return std::unique_lock<std::mutex>(m);
  }
  mutable std::mutex m;
  mutable std::condition_variable cv;
  T t;
};

现在

timer_killer
是:

struct timer_killer {
  enum class wait_result {
    timeout,
    killed,
  };
  // returns true if killed
  template<class R, class P>
  wait_result wait_for( std::chrono::duration<R,P> const& time ) const {
    return flag.wait_for(
      time,
      [](bool terminate){ return terminate; }
    )?wait_result::killed:wait_result::timeout;
  }
  void kill() {
    flag.modify(
      [](bool& terminate){
        terminate=true;
        return notify::all;
      }
    );
  }
private:
  waitable<bool> flag = false;
};

这会将所有标准互斥体/条件变量/等逻辑移出该特定类的业务逻辑。

waitable
的用户只需编写一个函数来检查他们是否应该停止等待(并返回true),并编写一个函数来修改状态以指示等待结束(然后返回如果每个人,或者只有一个,或者没有,需要waiter来消费状态变化)。

我还将

wait_for
上的真/假回报更改为
enum
- 根据我的经验,哪个是真的,哪个是假是任意的,并且会导致很多错误。

waitable
看起来相对毫无意义和薄弱 - 但在实践中,它可以防止你“意外地”弄乱状态(
t
)或摆弄状态并且在等待时不通知或修改状态以及其他导致错误的事情。 另外,因为相对于彼此的锁定、读取、写入和通知方式很重要,更改它可能会导致微妙的错误,从而使编写定制代码变得更加困难,这样做是好的。


5
投票

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

您可以对条件变量使用定时等待,并让另一个线程向您的周期性线程发送信号,以便在时间到时唤醒并终止。

或者你也可以

poll
在管道上用睡眠作为超时而不是睡觉。然后,您只需向管道写入一个字节,线程就会唤醒并退出。

© www.soinside.com 2019 - 2024. All rights reserved.