【问题标题】:How to exit from a background thread loop?如何退出后台线程循环?
【发布时间】:2015-08-25 14:22:03
【问题描述】:

我有一个用于上传文件的后台线程。它循环运行;它会做一些工作,然后休眠直到超时或通过条件变量明确通知它还有更多工作要做。问题是有时我无法让线程快速退出。

这是一个简化版:

    std::thread g_thread;
    std::mutex g_mutex;
    std::condition_variable g_cond;
    bool g_stop = false;

    void threadLoop()
    {   
        while (!g_stop)
        {   
            printf("doing some stuff\n");
            std::unique_lock<std::mutex> lock(g_mutex);
            g_cond.wait_for(lock, std::chrono::seconds(15));
        }   
    }   

    int main(int argc, char* argv[])
    {           
        g_stop = false;
        g_thread = std::thread(threadLoop);

        printf("hello\n");

        g_stop = true;
        g_cond.notify_one();
        g_thread.join();
    }

当我运行这个测试程序时,我希望它能够快速退出,但有时它会卡在 wait_for() 中。我认为可能 notify_one() 发生在线程在 wait_for() 中休眠之前,但在检查 g_stop 之后。

有没有一个简单的解决方案,或者其他更好的设计模式?

【问题讨论】:

  • 您应该将 g_stop 的类型更改为 atomic_bool 以避免未定义的行为,更不用说缓存问题了。或者只在持有互斥锁时读取它。
  • 我不确定 C++ 中的方法,但基本上当线程处于睡眠状态时,使其响应的唯一方法是发送信号。在 C 中,它将使用 int pthread_kill(pthread_t thread, int sig);
  • @AxFab,不,这没有帮助。它正在等待条件变量,因此您可以通过通知该条件变量来解除阻塞。
  • @JonathanWakely 是的,你必须设置g_stop = true。但是如果你想在等待的 15 秒之前退出,你也必须发送一个信号。
  • 一个大问题是没有正确使用条件变量。您需要在持有互斥锁的同时检查谓词(例如“有工作要做”或“我应该停止吗”),然后如果谓词为假,则 wait_for() 。条件变量不会保持信号状态,因此如果有人在您不在 .wait_for() 调用中时调用 .notify() - 您会错过通知。

标签: c++ multithreading stl condition-variable stdthread


【解决方案1】:

您在没有任何同步的情况下读写g_stop 变量(例如使用原子操作,或使用互斥锁序列化对其的访问)。这是一场数据竞赛,是未定义的行为。

因为你不能安全地访问它,所以允许编译器假设没有其他线程修改过g_stop,所以在threadLoop 函数中它可以将它加载到寄存器中一次,然后不再读取变量,但只是继续循环。

为确保循环线程可以看到对变量的写入,您应该使用std::atomic&lt;bool&gt; 或在对该变量进行所有读取/写入之前锁定互斥锁。如果您使用 atomic&lt;bool&gt; 来修复未定义的行为,但不能确保线程不会等待条件变量,因为正如您所建议的那样,在检查 g_stop 的值和进入睡眠之间存在一个窗口,其中主线程可以设置g_stop = true 并向condvar 发出信号,因此循环线程不会等到notify_one() 调用之后,因此会错过它。

这个稍作改动的版本将确保如果主线程告诉它停止,线程不会等待条件变量:

std::thread g_thread;
std::mutex g_mutex;
std::condition_variable g_cond;
bool g_stop = false;

void threadLoop()
{   
    std::unique_lock<std::mutex> lock(g_mutex);
    while (!g_stop)
    {   
        printf("doing some stuff\n");
        g_cond.wait_for(lock, std::chrono::seconds(15));
    }   
}   

int main(int argc, char* argv[])
{           
    g_stop = false;
    g_thread = std::thread(threadLoop);

    printf("hello\n");

    {
      std::lock_guard<std::mutex> lock(g_mutex);
      g_stop = true;
    }
    g_cond.notify_one();
    g_thread.join();
}

这是因为循环线程在检查 g_stop 时持有互斥锁上的锁,并且它一直持有该锁直到它开始等待 condvar。主线程获取锁来设置g_stop = true,它只能在其他线程等待时执行。

这意味着现在只有两种可能的执行方式。 g_stop = true 发生在线程等待 condvar 时,它要么在 notify_one() 调用之前唤醒,要么在 notify_one() 调用之前唤醒因为,但在这两种情况下它都会立即查看g_stop == true 并停止循环。

【讨论】:

  • 啊;我没有意识到 wait_for() 会释放锁。这对我来说很有意义。谢谢!关于 g_stop 的原子性的观点......我假设在大多数架构上默认情况下,布尔的对齐访问将是原子的;这是一个合理的假设吗? (即便如此,改用 atomic 并没有什么坏处,我会的。)
  • 另外..很好奇,你为什么在你的代码中选择 lock_guard 而不是 unique_lock?
  • 是的,等待条件变量会释放锁并进入睡眠状态。它以原子方式进行,因此主线程不可能重新锁定互斥锁,设置g_stop=true 并在解锁和等待之间发送通知。这意味着您不会错过通知......只要等待的条件(在这种情况下布尔值变为真)发生在与条件变量等待相同的互斥锁的保护下。这是@nos 在上面的评论中提到的关于正确使用条件变量的一部分。
  • 假设对齐的 1 字节访问是原子的可能是安全的,但这并不是您需要担心的全部。使用适当的原子操作可以处理内存可见性和同步,而不仅仅是确保没有部分读/写。最后,我在主线程中使用了lock_guard,因为它比unique_lock 更简单,因此(非常稍微)更快。 unique_lock 可以在其生命周期内解锁/重新锁定,这是与 condition_variable 一起使用它所必需的,但在您只想在构造时锁定它并在破坏时解锁的主线程中不是必需的。
猜你喜欢
  • 2013-12-06
  • 2013-09-08
  • 2012-05-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-07-26
  • 1970-01-01
相关资源
最近更新 更多