Stopping long-sleep threads

14,159

Solution 1

Use a condition variable. You wait on the condition variable or 5 minutes passing. Remember to check for spurious wakeups.

cppreference

I cannot find a good stack overflow post on how to use a condition variable in a minute or two of google searching. The tricky part is realizing that the wait can wake up with neither 5 minutes passing, nor a signal being sent. The cleanest way to handle this is to use the wait methods with a lambda that double-checks that the wakeup was a "good" one.

here is some sample code over at cppreference that uses wait_until with a lambda. (wait_for with a lambda is equivalent to wait_until with a lambda). I modified it slightly.

Here is an version:

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;
};

live example.

You create a timer_killer in a shared spot. Client threads can wait_for( time ). If it returns false, it means you where killed before your wait was complete.

The controlling thread just calls kill() and everyone doing a wait_for gets a false return.

Note that there is some contention (locking of the mutex), so this isn't suitable for infinite threads (but few things are). Consider using a scheduler if you need to have an unbounded number of tasks that are run with arbitrary delays instead of a full thread per delayed repeating task -- each real thread is upwards of a megabyte of system address space used (just for the stack).

Solution 2

There are two traditional ways you could do this.

You could use a timed wait on a condition variable, and have the other thread signal your periodic thread to wake up and die when it's time.

Alternately you could poll on a pipe with your sleep as a timeout instead of of sleeping. Then you just write a byte to the pipe and the thread wakes up and can exit.

Share:
14,159
PaperBirdMaster
Author by

PaperBirdMaster

Updated on July 16, 2022

Comments

  • PaperBirdMaster
    PaperBirdMaster almost 2 years

    Let's suppose I have a thread which should perform some task periodically but this period is 6 times each hour 12 times each hour (every 5 minutes), I've often seen code which controls the thread loop with a is_running flag which is checked every loop, like this:

    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);
        }
    }
    

    But if the stop() function is called, let's say, 1 millisecond after start() the thread would be alive for 299999 additional milliseconds until it awakes, checks the flag, and die.

    Is my understanding correct? How to avoid keeping alive (but sleeping) a thread which should have been ended? My best approach until now is the following:

    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);
            }
        }
    }
    

    Is there a less-dirty and more straightforward way to achieve this?