【问题标题】:Worker Thread permanently hibernates, after executing too fast工作线程在执行太快后永久休眠
【发布时间】:2018-09-21 02:28:34
【问题描述】:

我正在尝试将线程合并到我的项目中,但有一个问题,即仅使用 1 个工作线程使其永久“入睡”。也许我有一个竞争条件,但只是没有注意到它。

我的PeriodicThreads 对象维护着一个线程集合。一旦PeriodicThreads::exec_threads() 被调用,线程就会被通知,被唤醒并执行它们的任务。之后,他们又睡着了。

这样一个工作线程的功能:

void PeriodicThreads::threadWork(size_t threadId){
    //not really used, but need to decalre to use conditional_variable:
    std::mutex mutex;
    std::unique_lock<std::mutex> lck(mutex);

    while (true){
        // wait until told to start working on a task:
        while (_thread_shouldWork[threadId] == false){
            _threads_startSignal.wait(lck);
        }

        thread_iteration(threadId);    //virtual function

        _thread_shouldWork[threadId] = false;   //vector of flags
        _thread_doneSignal.notify_all();

    }//end while(true) - run until terminated externally or this whole obj is deleted 
}

如您所见,每个线程都在监视标志向量中的自己的条目,一旦它看到它的标志为真 - 执行任务然后重置其标志。

这里是可以唤醒所有线程的函数:

std::atomic_bool _threadsWorking =false;

//blocks the current thread until all worker threads have completed:
void PeriodicThreads::exec_threads(){
    if(_threadsWorking ){ 
        throw std::runtime_error("you requested exec_threads(), but threads haven't yet finished executing the previous task!");
    }

    _threadsWorking = true;//NOTICE: doing this after the exception check.

    //tell all threads to unpause by setting their flags to 'true'
    std::fill(_thread_shouldWork.begin(),  _thread_shouldWork.end(),  true);
    _threads_startSignal.notify_all();

    //wait for threads to complete:

    std::mutex mutex;
    std::unique_lock<std::mutex> lck(mutex); //lock & mutex are not really used.

    auto isContinueWaiting = [&]()->bool{
        bool threadsWorking = false; 
        for (size_t i=0;  i<_thread_shouldWork.size();  ++i){
            threadsWorking |= _thread_shouldWork[i];
        }
        return threadsWorking;
    };

    while (isContinueWaiting()){
        _thread_doneSignal.wait(lck);
    }

    _threadsWorking = false;//set atomic to false 
}

调用exec_threads() 可以正常进行数百次或在极少数情况下数千次连续迭代。调用发生在主线程的while 循环中。它的工作线程处理任务,重置其标志并返回休眠状态,直到下一个exec_threads(),依此类推。

但是,在那之后的一段时间,程序突然进入“休眠”状态,并且似乎暂停了,但没有崩溃。

在这种“休眠”期间,在我的 condition_variables 的任何 while-loop 处设置断点实际上不会触发该断点。


偷偷摸摸,我创建了自己的验证线程(与main 平行)并监控我的PeriodicThreads 对象。当它进入休眠状态时,我的验证线程不断向控制台输出当前没有线程正在运行的消息(PeriodicThreads_threadsWorking atomic 永久设置为 false)。但是,在其他测试期间,一旦“休眠问题”开始,原子仍为 true

奇怪的是,如果我强制PeriodicThreads::run_thread 在重置其标志之前至少休眠 10 微秒,一切都会正常工作,并且不会发生“休眠”。否则,如果我们允许线程非常快地完成它的任务,它可能会导致整个问题。

我已将每个 condition_variable 包装在一个 while 循环中,以防止虚假唤醒触发转换,以及在调用 .wait() 之前调用 notify_all 的情况。 Link

注意,即使我只有 1 个工作线程也会发生这种情况

可能是什么原因?

编辑

放弃这些向量标志并仅在具有 1 个工作线程的单个 atomic_bool 上进行测试仍然会出现同样的问题。

【问题讨论】:

  • 如果我有什么有用的话,我会在读完之后回来。与此同时,我已将每个 condition_variable 包装在一个 while 循环中以防止虚假唤醒 不需要! std::condition_variable::wait has an overload that will do this for you. 给它一个快乐的小 lambda 来测试退出条件,然后你就走了!例如:_thread_doneSignal.wait(lck); 变成了_thread_doneSignal.wait(lck, isContinueWaiting ); 从我到目前为止所做的一瞥。
  • 谢谢!我最初使用了第二个重载,但不确定notify_all 是否会忽略该检查,换句话说,它是否只是用于虚假唤醒。所以我把它移到了while循环中,以防在另一个线程有机会在这个条件变量上wait之前调用notify_all。我的担忧有效吗?
  • lambda“谓词”重载等效于将 cv wait 包装在 while 循环中。它们确实是同一件事 :) 值得注意的是,这不仅仅是因为虚假唤醒。使用条件变量时,必须确保条件仍然成立(Mesa 语义)。线程 X 有可能在线程 Y 被“唤醒”之后但在线程 Y 有机会获得锁之前锁定->修改->解锁条件状态。所以线程Y不能假设条件状态,它必须在抢到锁后再次检查。

标签: c++ multithreading condition-variable


【解决方案1】:

所有共享数据都应受互斥体保护。互斥体应该(至少)与共享数据具有相同的范围。

您的_thread_shouldWork 容器是共享数据。您可以创建一个全局互斥体数组,每个互斥体都可以保护自己的_thread_shouldWork 元素。 (见下面的注释)。您还应该至少拥有与拥有互斥锁一样多的条件变量。 (您可以将 1 个互斥锁与多个不同的条件变量一起使用,但您不应将多个不同的互斥锁与 1 个条件变量一起使用。)

condition_variable 应该保护 实际 条件(在这种情况下,_thread_shouldWork 的单个元素在任何给定点的状态)并且互斥锁用于保护包含那个条件。

如果您只是使用随机的本地互斥锁(就像您在线程代码中一样)或根本不使用互斥锁(在主代码中),那么所有的赌注都没有了。这是未定义的行为。虽然我可以看到它大部分时间都在工作(幸运)。我怀疑正在发生的是工作线程缺少来自主线程的信号。也可能是您的主线程缺少来自工作线程的信号。 (线程A读取状态并进入while循环,然后线程B改变状态并发送通知,然后线程A进入睡眠...等待已经发送的通知)

具有本地范围的互斥体是一个危险信号!

注意:如果您使用的是矢量,则必须小心,因为添加或删除项目可能会触发调整大小,这将在不先抓取互斥锁的情况下触及元素(因为当然矢量不知道您的互斥锁) .

使用数组时也要注意虚假共享

编辑:这是@Kari 发现对解释虚假分享有用的视频 https://www.youtube.com/watch?v=dznxqe1Uk3E

【讨论】:

  • 按照您的建议,我已经解决了我的问题。 1 个附加问题 - 对于我的特殊情况,我应该在 之后 notify_all() 还是之前解锁锁?我在这里问过stackoverflow.com/q/52503361/9007125
  • 在互斥锁解锁后调用notify_allnotify_one也是如此)效率更高。休眠线程被唤醒后要做的第一件事就是尝试获取互斥锁。如果该互斥锁仍被唤醒它的线程持有,则唤醒线程将立即再次阻塞,直到唤醒器释放互斥锁。如需更完整的解释,请查看我对这个问题的回答stackoverflow.com/questions/50580656/…(我不喜欢在这里使用notify_all。)
  • 另外,我没有提到您还需要一个条件变量数组。在同一个条件变量上使用不同的互斥锁是灾难的根源。我刚刚编辑了我的答案以添加这一点。
  • Launcher 锁定互斥锁,迭代标志并在某些标志仍然为真时继续等待。这在我的笔记本电脑上的每个工作线程消耗 5 微秒,我认为效果很好。
  • 我现在将验证在解锁互斥锁后调用 notify_all 不会导致问题(例如工作线程意外执行两次),谢谢!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-11-12
  • 1970-01-01
  • 2011-09-11
  • 1970-01-01
  • 1970-01-01
  • 2020-12-06
相关资源
最近更新 更多