【问题标题】:How to wake a std::thread while it is sleeping如何在睡眠时唤醒 std::thread
【发布时间】:2018-10-02 14:43:49
【问题描述】:

我正在使用 C++11,我有一个 std::thread,它是一个类成员,它每 2 分钟向听众发送一次信息。其他的,它只是睡觉。所以,我让它休眠 2 分钟,然后发送所需的信息,然后再次休眠 2 分钟。

// MyClass.hpp
class MyClass {

    ~MyClass();
    RunMyThread();

private:
    std::thread my_thread;
    std::atomic<bool> m_running;
}


MyClass::RunMyThread() {

    my_thread = std::thread { [this, m_running] {
    m_running = true;
    while(m_running) {
        std::this_thread::sleep_for(std::chrono::minutes(2));
        SendStatusInfo(some_info);
    }
}};
}

// Destructor
~MyClass::MyClass() {
    m_running = false; // this wont work as the thread is sleeping. How to exit thread here?
}

问题:
这种方法的问题是我无法在线程睡眠时退出线程。我从阅读中了解到,我可以使用std::condition_variable 唤醒它并优雅地退出?但我正在努力寻找一个 simple example 来满足上述情况的要求。我发现的所有condition_variable 示例对于我在这里尝试做的事情来说看起来都太复杂了。

问题:
如何使用std::condition_variable 唤醒线程并在其休眠时优雅退出?或者有没有其他方法可以在没有condition_variable 技术的情况下达到同样的效果?

另外,我发现我需要将std::mutexstd::condition_variable 结合使用?这真的有必要吗?是不是只在代码中需要的地方添加std::condition_variable逻辑就不能达到目的?

环境:
带有编译器 gcc 和 clang 的 Linux 和 Unix。

【问题讨论】:

  • 请注意,您显示的代码充满了语法错误。
  • 您在 Windows 上吗?那里有一些操作系统原语可以帮助您。
  • 作为std::condition_variable 的替代品 - 对于这种需要一次性信号的情况,您可以使用std::promise&lt;void&gt;std::future::wait_for()
  • @PaulSanders 我的代码需要在 linux 和 unix 上运行。我用开发环境的详细信息更新了问题
  • @FrançoisAndrieux 是的。我正在处理那部分。因为这个问题的重点并不是真正的主题,所以我没有关注那个。无论如何,感谢您添加这一点

标签: c++ c++11 mutex condition-variable stdthread


【解决方案1】:

如何使用std::condition_variable 唤醒线程并在其休眠时优雅地退出?或者有没有其他方法可以在没有condition_variable 技术的情况下达到同样的效果?

不,从 C++17 开始,标准 C++ 中没有(当然有非标准的、特定于平台的方法,并且很可能会在 C++2a 中添加某种信号量)。

另外,我发现我需要将std::mutexstd::condition_variable 结合使用?真的有必要吗?

是的。

难道不可以通过将std::condition_variable 逻辑只添加到此处代码段中所需的位置来实现目标吗?

没有。首先,如果不锁定互斥锁(并将锁定对象传递给等待函数),您将无法等待condition_variable,因此无论如何您都需要存在互斥锁。由于无论如何您都必须有一个互斥锁,因此要求服务员和通知者都使用该互斥锁并不是什么大问题。

条件变量会受到“虚假唤醒”的影响,这意味着它们可以无缘无故地停止等待。为了判断它是因为被通知而唤醒,还是被虚假唤醒,您需要一些由通知线程设置并由等待线程读取的状态变量。因为该变量由多个线程共享,所以需要安全地访问它,而互斥锁确保了这一点。

即使您对共享变量使用原子变量,通常仍需要互斥锁以避免错过通知。

这一切在 https://github.com/isocpp/CppCoreGuidelines/issues/554

【讨论】:

  • “即使您使用原子变量作为共享变量,您通常仍然需要互斥体以避免错过通知。” 确实是最佳答案。如果你有一个线程安全的条件,通常认为你不需要互斥锁,这会导致可怕的头痛。编辑:似乎斯拉瓦的回答也提到了它。
  • “spurios 醒来”是什么意思?我从来没有听说过这个。这样的事情不会让condition_variable 毫无意义吗?
  • en.wikipedia.org/wiki/Spurious_wakeup——不,这并不意味着它毫无意义,因为只要你正确使用条件变量,一切都会正常工作(通过将它与实际条件相关联,例如“共享变量非零”并使用互斥锁来同步对共享变量的访问并防止错过通知)。
  • 为了完整起见,还有std::condition_variable_any 不需要std::mutex,而是适用于任何满足BasicLockable 概念的锁。效率可能会有所不同。
【解决方案2】:

一个使用std::condition_variable的工作示例:

struct MyClass {
    MyClass()
        : my_thread([this]() { this->thread(); })
    {}

    ~MyClass() {
        {
            std::lock_guard<std::mutex> l(m_);
            stop_ = true;
        }
        c_.notify_one();
        my_thread.join();
    }

    void thread() {
        while(this->wait_for(std::chrono::minutes(2)))
            SendStatusInfo(some_info);
    }

    // Returns false if stop_ == true.
    template<class Duration>
    bool wait_for(Duration duration) {
        std::unique_lock<std::mutex> l(m_);
        return !c_.wait_for(l, duration, [this]() { return stop_; });
    }

    std::condition_variable c_;
    std::mutex m_;
    bool stop_ = false;
    std::thread my_thread;
};

【讨论】:

  • l不需要加锁解锁?如果不需要但可能需要更简单?
  • @Game_Of_Threads No. 仔细阅读文档:en.cppreference.com/w/cpp/thread/condition_variable/wait_for
  • 好的。我现在明白了。对于条件变量,锁更像是强制性的,因为它本身会在所需条件下锁定和释放。谢谢@Maxim Egorushkin
  • @MaximEgorushkin 你有什么理由在析构函数中使用lock_guard,在wait_for 中使用unique_lock
  • @Youda008 lock_guard比较简单,但是wait_for不能取lock_guard
【解决方案3】:

如何使用 std::condition_variable 唤醒线程并在其休眠时优雅退出?

你使用std::condition_variable::wait_for()而不是std::this_thread::sleep_for(),第一个可以被std::condition_variable::notify_one()std::condition_variable::notify_all()打断

另外,我发现我需要将 std::mutex 与 std::condition_variable 结合使用?这真的有必要吗?在这里的代码片段中只添加std::condition_variable逻辑是不是不能达到目的?

是的,有必要将std::mutexstd::condition_variable 一起使用,并且您应该使用它而不是制作您的标志std::atomic,因为尽管标志本身具有原子性,但您的代码中会出现竞争条件,您会注意到有时您的如果您不在这里使用互斥锁,睡眠线程会错过通知。

【讨论】:

  • std::condition_variable::wait_for()。我想这可能是我需要的。
【解决方案4】:

有一个可悲但真实的事实 - 您正在寻找的是一个信号,而 Posix 线程没有真正的信号机制。

此外,与任何时间相关的唯一 Posix 线程原语是条件变量,这就是为什么您的在线搜索会引导您找到它,并且由于 C++ 线程模型在标准 C++ Posix 兼容原语中大量构建在 Posix API 之上就是你得到的。

除非您愿意脱离 Posix(您没有指明平台,但有本地平台方法可以处理不受这些限制的事件,尤其是 Linux 中的 eventfd),您必须遵守条件变量,是的,使用条件变量需要一个互斥锁,因为它内置在 API 中。

您的问题并未专门要求提供代码示例,因此我不提供任何示例。如果你想要一些,请告诉我。

【讨论】:

  • 啊,这是 posix 约束。不幸的是,我只停留在 C++ 领域。似乎我的问题以某种方式没有正确指出作为答案的一部分,我正在寻找对我提供的示例的代码修改。但是没关系。我正在尝试使用 Siava 的回答中指出的std::condition_variable::wait_for()
  • 非常有趣的基本事实。至少我现在可以确定我在查看所有 condition_var 示例时猜测的一些事情
  • 正如 Jonathan Wakely 之前提到的,考虑将信号量类包含在(可能会是)C++20 中,这将使基本的线程间信号变得更加重要实施起来不那么痛苦。它还可以很好地映射到现有的 POSIX 信号量,这意味着可能会提高性能和调试工具的兼容性(尤其是更好的 Helgrind 支持)。
  • 这应该是公认的答案,因为该帖子的作者明确要求线程在睡眠时唤醒的机制。
【解决方案5】:

另外,我发现我需要将 std::mutex 与 std::condition_variable 结合使用?这真的有必要吗?在这里的代码片段中只添加std::condition_variable逻辑是不是不能达到目的?

std::condition_variable 是一个低级原语。实际上使用它也需要摆弄其他低级原语。

struct timed_waiter {
  void interrupt() {
    auto l = lock();
    interrupted = true;
    cv.notify_all();
  }
  // returns false if interrupted
  template<class Rep, class Period>
  bool wait_for( std::chrono::duration<Rep, Period> how_long ) const {
    auto l = lock();
    return !cv.wait_until( l,
      std::chrono::steady_clock::now() + how_long,
      [&]{
        return !interrupted;
      }
    );
  }
private:
  std::unique_lock<std::mutex> lock() const {
    return std::unique_lock<std::mutex>(m);
  }
  mutable std::mutex m;
  mutable std::condition_variable cv;
  bool interrupted = false;
};

只需在某个地方创建一个timed_waiter,想要等待的线程和想要中断的代码都可以看到它。

等待线程做

while(m_timer.wait_for(std::chrono::minutes(2))) {
    SendStatusInfo(some_info);
}

中断m_timer.interrupt()(在dtor中说)然后my_thread.join()让它完成。

Live example:

struct MyClass {
    ~MyClass();
    void RunMyThread();
private:
    std::thread my_thread;
    timed_waiter m_timer;
};


void MyClass::RunMyThread() {

    my_thread = std::thread {
      [this] {
      while(m_timer.wait_for(std::chrono::seconds(2))) {
        std::cout << "SendStatusInfo(some_info)\n";
      }
    }};
}

// Destructor
MyClass::~MyClass() {
    std::cout << "~MyClass::MyClass\n";
    m_timer.interrupt();
    my_thread.join();
    std::cout << "~MyClass::MyClass done\n";
}

int main() {
    std::cout << "start of main\n";
    {
        MyClass x;
        x.RunMyThread();
        using namespace std::literals;
        std::this_thread::sleep_for(11s);
    }
    std::cout << "end of main\n";
}

【讨论】:

    【解决方案6】:

    或者有没有其他方法可以在没有条件变量技术的情况下达到同样的效果?

    在这种情况下,您可以使用std::promise/std::future 作为bool/condition_variable/mutex 的更简单替代方案。 future 不易受到虚假唤醒的影响,并且不需要 mutex 进行同步。

    基本示例:

    std::promise<void> pr;
    std::thread thr{[fut = pr.get_future()]{
        while(true)
        {
            if(fut.wait_for(std::chrono::minutes(2)) != std::future_status::timeout)
                return;
        }
    }};
    //When ready to stop
    pr.set_value();
    thr.join();
    

    【讨论】:

      【解决方案7】:

      或者有没有其他方法可以在没有条件变量技术的情况下达到同样的效果?

      条件变量的一种替代方法是,您可以以更规律的时间间隔唤醒线程以检查“正在运行”标志,如果未设置且分配的时间已过,则返回睡眠状态尚未过期:

      void periodically_call(std::atomic_bool& running, std::chrono::milliseconds wait_time)
      {
          auto wake_up = std::chrono::steady_clock::now();
      
          while(running)
          {
              wake_up += wait_time; // next signal send time
      
              while(std::chrono::steady_clock::now() < wake_up)
              {
                  if(!running)
                      break;
      
                  // sleep for just 1/10 sec (maximum)
                  auto pre_wake_up = std::chrono::steady_clock::now() + std::chrono::milliseconds(100);
      
                  pre_wake_up = std::min(wake_up, pre_wake_up); // don't overshoot
      
                  // keep going to sleep here until full time
                  // has expired
                  std::this_thread::sleep_until(pre_wake_up);
              }
      
              SendStatusInfo(some_info); // do the regular call
          }
      }
      

      注意:您可以任意设置实际等待时间。在此示例中,我将其设为 100 毫秒 std::chrono::milliseconds(100)。这取决于您希望线程对停止信号的响应程度。

      例如,在一个应用程序中,我做了整整一秒钟,因为我很高兴我的应用程序在退出时关闭之前等待整整一秒钟,以便所有线程停止。

      您需要的响应速度取决于您的应用程序。唤醒时间越短,消耗的CPU 就越多。然而,即使是几毫秒的非常短的时间间隔,在CPU 时间方面也可能不会记录太多。

      【讨论】:

        【解决方案8】:

        你也可以使用promise/future,这样你就不需要关心条件和/或线程:

        #include <future>
        #include <iostream>
        
        struct MyClass {
        
            ~MyClass() {
                _stop.set_value();
            }
        
            MyClass() {
                auto future = std::shared_future<void>(_stop.get_future());
                _thread_handle = std::async(std::launch::async, [future] () {
                    std::future_status status;
                    do {
                        status = future.wait_for(std::chrono::seconds(2));
                        if (status == std::future_status::timeout) {
                            std::cout << "do periodic things\n";
                        } else if (status == std::future_status::ready) {
                            std::cout << "exiting\n";
                        }
                    } while (status != std::future_status::ready);
                });
            }
        
        
        private:
            std::promise<void> _stop;
            std::future<void> _thread_handle;
        };
        
        
        // Destructor
        int main() {
            MyClass c;
            std::this_thread::sleep_for(std::chrono::seconds(9));
        }
        

        【讨论】:

          猜你喜欢
          • 2015-11-20
          • 2012-10-08
          • 2014-07-29
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2014-11-11
          • 2011-06-01
          相关资源
          最近更新 更多