Approach of using an std::atomic compared to std::condition_variable wrt pausing & resuming an std::thread in C++

☆樱花仙子☆ 提交于 2019-12-23 09:25:30

问题


This is a separate question but related to the previous question I asked here

I am using an std::thread in my C++ code to constantly poll for some data & add it to a buffer. I use a C++ lambda to start the thread like this:

StartMyThread() {

    thread_running = true;
    the_thread = std::thread { [this] {
        while(thread_running) {
          GetData();
        }
    }};
}

thread_running is an atomic<bool> declared in class header. Here is my GetData function:

GetData() {
    //Some heavy logic
}

Next I also have a StopMyThread function where I set thread_running to false so that it exits out of the while loop in the lambda block.

StopMyThread() {
  thread_running = false;
  the_thread.join();
}

As I understand, I can pause & resume the thread using a std::condition_variable as pointed out here in my earlier question.

But is there a disadvantage if I just use the std::atomic<bool> thread_running to execute or not execute the logic in GetData() like below ?

GetData() {
    if (thread_running == false)
      return;
    //Some heavy logic
}

Will this burn more CPU cycles compared to the approach of using an std::condition_variable as described here ?


回答1:


A condition variable is useful when you want to conditionally halt another thread or not. So you might have an always-running "worker" thread that waits when it notices it has nothing to do to be running.

The atomic solution requires your UI interaction synchronize with the worker thread, or very complex logic to do it asynchronously.

As a general rule, your UI response thread should never block on non-ready state from worker threads.

struct worker_thread {
  worker_thread( std::function<void()> t, bool play = true ):
    task(std::move(t)),
    execute(play)
  {
    thread = std::async( std::launch::async, [this]{
      work();
    });
  }
  // move is not safe.  If you need this movable,
  // use unique_ptr<worker_thread>.
  worker_thread(worker_thread&& )=delete;
  ~worker_thread() {
    if (!exit) finalize();
    wait();
  }
  void finalize() {
    auto l = lock();
    exit = true;
    cv.notify_one();
  }
  void pause() {
    auto l = lock();
    execute = false;
  }
  void play() {
    auto l = lock();
    execute = true;
    cv.notify_one();
  }
  void wait() {
    Assert(exit);
    if (thread)
      thread.get();
  }
private:
  void work() {
    while(true) {
      bool done = false;
      {
        auto l = lock();
        cv.wait( l, [&]{
          return exit || execute;
        });
        done = exit; // have lock here
      }
      if (done) break;
      task();
    }
  }
  std::unique_lock<std::mutex> lock() {
     return std::unique_lock<std::mutex>(m);
  }
  std::mutex m;
  std::condition_variable cv;
  bool exit = false;
  bool execute = true;
  std::function<void()> task;
  std::future<void> thread;
};

or somesuch.

This owns a thread. The thread repeatedly runs task so long as it is in play() mode. If you pause() the next time task() finishes, the worker thread stops. If you play() before the task() call finishes, it doesn't notice the pause().

The only wait is on destruction of worker_thread, where it automatically informs the worker thread it should exit and it waits for it to finish.

You can manually .wait() or .finalize() as well. .finalize() is async, but if your app is shutting down you can call it early and give the worker thread more time to clean up while the main thread cleans things up elsewhere.

.finalize() cannot be reversed.

Code not tested.




回答2:


Unless I'm missing something, you already answered this in your original question: You'll be creating and destroying the worker thread each time it's needed. This may or may not be an issue in your actual application.




回答3:


There's two different problems being solved and it may depend on what you're actually doing. One problem is "I want my thread to run until I tell it to stop." The other seems to be a case of "I have a producer/consumer pair and want to be able to notify the consumer when data is ready." The thread_running and join method works well for the first of those. The second you may want to use a mutex and condition because you're doing more than just using the state to trigger work. Suppose you have a vector<Work>. You guard that with the mutex, so the condition becomes [&work] (){ return !work.empty(); } or something similar. When the wait returns, you hold the mutex so you can take things out of work and do them. When you're done, you go back to wait, releasing the mutex so the producer can add things to the queue.

You may want to combine these techniques. Have a "done processing" atomic that all of your threads periodically check to know when to exit so that you can join them. Use the condition to cover the case of data delivery between threads.



来源:https://stackoverflow.com/questions/40553609/approach-of-using-an-stdatomic-compared-to-stdcondition-variable-wrt-pausing

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!