0

I am implementing a timer library inspired by Simple Timer

#ifndef CMS_TIMER
#define CMS_TIMER

#include <thread>
#include <chrono>
#include <atomic>

class Timer {
    std::atomic_bool clear = false;

public:
    template<class Function, class... Args, class Rep, class Period>
    [[maybe_unused]] void setInterval(const std::chrono::duration<Rep, Period>& sleep_duration, Function&& f, Args&&... args);
    void stop();
};

template<class Function, class... Args, class Rep, class Period>
void Timer::setInterval(const std::chrono::duration<Rep, Period>& sleep_duration, Function&& f, Args&&... args) {
    this->clear = false;
    std::thread t([std::move(args)]() {
        while(true) {
            if (this->clear) return;
            std::this_thread::sleep_for(sleep_duration);
            if(this->clear) return;
            f(args);
        }
    });
    t.detach();
}

void Timer::stop() {
    this->clear = true;
}
#endif

But I am not sure what to use instead of [std::move(args)] I need to function and arguments them by move semantics,

This answer didn't help me

Sahib Yar
  • 1,030
  • 11
  • 29
  • Why do you think you need something different? It's not really clear what the problem is here. – Asteroids With Wings Jan 18 '21 at 18:47
  • @AsteroidsWithWings because the placement of the `...` expansion is weird. – HTNW Jan 18 '21 at 18:48
  • @HTNW "Weird" how? Can we have some specifics please? – Asteroids With Wings Jan 18 '21 at 18:48
  • Why can't you just put an = in there and pass by value? – Joseph Larson Jan 18 '21 at 18:49
  • by using [=] I am getting Initializers contain unexpanded parameter pack `args` – Sahib Yar Jan 18 '21 at 18:51
  • 1
    Note: If you use `sleep_for` your timing won't be very accurate. Use `sleep_until` instead and use a timepoint that ju just add a fixed duration to instead. – Ted Lyngmo Jan 18 '21 at 18:53
  • @AsteroidsWithWings In C++20, the `...` would go like `[...args = std::move(args)]() { /* ... */ f(args...); }` which is entirely non-obvious and is not covered in the linked question. Apparently that syntax doesn't exist in C++17, which is why the `std::tuple` mess is needed. @JosephLarson that would copy. – HTNW Jan 18 '21 at 18:56

1 Answers1

4
template<class Function, class... Args, class Rep, class Period>
void Timer::setInterval(const std::chrono::duration<Rep, Period>& sleep_duration, Function&& f, Args&&... args) {
    this->clear = false;
    std::thread t([this, f=std::move(f), sleep_duration, args = std::make_tuple(std::forward<Args>(args)...))]() {
        while(true) {
            if (this->clear) return;
            std::this_thread::sleep_for(sleep_duration);
            if(this->clear) return;
            std::apply(f, args);
        }
    });
    t.detach();
}

That should do correct capturing. Note that as the timer goes off more than once, you don't want to further-forward data into the f.

But that still doesn't work because you didn't capture this.

Really, you should do a proper cv/mutex and not leak threads.

class Timer {
  std::condition_variable cv;
  mutable std::mutex m;
  bool clear = false;
  std::thread active_thread;
  void dispose_thread() {
    if (active_thread.joinable()) {
      stop();
      active_thread.join();
    }
    clear = false; // safe, because all threads are dead
  }
public:
    template<class Function, class... Args, class Rep, class Period>
    [[maybe_unused]] void setInterval(const std::chrono::duration<Rep, Period>& sleep_duration, Function&& f, Args&&... args);
    auto lock() const {
      return std::unique_lock( m );
    }
    void stop() {
      auto l = lock();
      clear = true;
      cv.notify_all();
    }
    ~Timer() { dispose_thread(); }
};

template<class Function, class... Args, class Rep, class Period>
void Timer::setInterval(const std::chrono::duration<Rep, Period>& sleep_duration, Function&& f, Args&&... args) {
    dispose_thread();
    // capture everything explicitly; when messing with threads,
    // best to understand state
    active_thread = std::thread(
      [this, f = std::move(f), sleep_duration, args = std::make_tuple(std::forward<Args>(args)...)]()
      {
        while(true) {
          {
            auto l = lock();
            // return value here isn't useful, but the function is:
            (void)cv.wait_for(l, sleep_duration, [&]{ return clear; });
            if (clear)
              return;
          }
          std::apply(f, args);
        }
      }
    );
}

and .stop() and .join() in the destructor.

Live example.

This prevents you from leaking threads and outliving your timer object and doesn't make you wait for a timer interval to set a new interval and the like.

You should also set up sleep_until time points, as if you want to do something every second and the f function takes 0.1 seconds, this will actually repeat every 1.1 seconds.

If you want to maintain multiple worker threads in one Timer object, I'd switch to using std::futures in a vector, and maybe sweeping them to clean up ones that are finished at various intervals.

The idea of waiting for existing tasks to cleanup before starting a new tasks has value to me. If you don't like that, you could have the threads report back that they are ready to clean up, and clean them up lazily later. But this also requires multiple clear variables, so the threads that are delayed don't get the wrong value when the next thread is ready to go.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • Updated code is not compile able, it is saying may be there is an extra ) while creating a std::thread, – Sahib Yar Jan 18 '21 at 19:09
  • @SahibYar Yes, I missed the `)` on the `if`. – Yakk - Adam Nevraumont Jan 18 '21 at 19:41
  • @SahibYar Also had to modify the wait syntax -- we are waiting for the timeout (A) or the signal (B), and the lambda-based `wait_for` assumes that we are waiting for the signal only. – Yakk - Adam Nevraumont Jan 18 '21 at 19:49
  • Could you post an example usage of setInterval? I'm unable to compile it at all, latest c++. Thanks – Mecanik Jan 07 '22 at 08:48
  • "error C3493: 'sleep_duration' cannot be implicitly captured because no default capture mode has been specified" and so on... – Mecanik Jan 07 '22 at 08:58
  • Please edit your answer so people can correctly compile it... active_thread = std::thread( [this, sleep_duration, f, args = std::make_tuple(std::forward(args)...)]() – Mecanik Jan 07 '22 at 09:22
  • @Mecanik Live example added. – Yakk - Adam Nevraumont Jan 07 '22 at 14:02
  • @Yakk-AdamNevraumont Thanks for editing the post, it was really annoying. Also, thanks for the example. However, it seems there is an issue with arguments. I`m unable to use arguments at all, it just won't compile. I tried to replace the tuple with C++20 style, but no success. If the arguments would work as well, this would be one hell of a solution as the timer works perfectly. – Mecanik Jan 07 '22 at 14:58
  • @Mecanik I just added an argument and it worked? https://godbolt.org/z/Mfz3MGrP7 -- are you using arguments that require perfect forwarding? That doesn't make sense because the timer fires more than once. – Yakk - Adam Nevraumont Jan 07 '22 at 15:48
  • One last bit, VS alerts the function " auto lock() const { return std::unique_lock(m); }" as "failed to release lock". – Mecanik Jan 07 '22 at 16:25
  • @Yakk-AdamNevraumont Any chance you can advise on that warning about the lock? Also, is there a way to do a simple std::cout showing the "time until next run"? Thanks – Mecanik Jan 29 '22 at 12:14
  • @meca I do not know why your specific version of VS has a bug. I'd advise using the [ask question] button about it. – Yakk - Adam Nevraumont Jan 29 '22 at 13:16
  • @Yakk-AdamNevraumont I`m using latest VS 2019 and latest C++, unfortunately this is a problem because the timer really get's stuck randomly... – Mecanik Jan 29 '22 at 16:09