sdbbs
sdbbs

Reputation: 5384

Calling a function after updateable delay in C++?

Below is my example for "calling a function after updateable delay": the idea is that,

So, if I do the very first press of a key and start the delay, then after 1 second press a key again, then after 2 seconds press a key again, then after 1 second press a key again, and then stop pressing keys - from this point the code should still wait 3 seconds before running the function to delay (so in this example, a total of 1+2+1+3 = 7 seconds would have expired from the very first press of a key).

Unfortunately, the code does not work as intended:

// compile on Linux with:
// g++ test.cpp -o test.exe -pthread

#include <iostream>
#include <chrono>
#include <thread>
#include <functional>

#include <sys/ioctl.h> // FIONREAD
#include <sys/select.h>
#include <termios.h>
#include <stropts.h>

std::chrono::time_point<std::chrono::steady_clock> tnow;
std::chrono::time_point<std::chrono::steady_clock> tend;
unsigned int interval_ms = 3000;
bool do_stop = false;
bool timer_started = false;

int _kbhit() { // https://www.flipcode.com/archives/_kbhit_for_Linux.shtml
  static const int STDIN = 0;
  static bool initialized = false;

  if (! initialized) {
    // Use termios to turn off line buffering
    termios term;
    tcgetattr(STDIN, &term);
    term.c_lflag &= ~ICANON;
    tcsetattr(STDIN, TCSANOW, &term);
    setbuf(stdin, NULL);
    initialized = true;
  }

  int bytesWaiting;
  ioctl(STDIN, FIONREAD, &bytesWaiting);
  return bytesWaiting;
}

void update_timing() {
  std::cout << "updating timer" << std::endl;
  tnow = std::chrono::steady_clock::now();
  tend = tnow + std::chrono::milliseconds(interval_ms);
}

void timer_start(std::function<void(void)> func) // inspired by https://stackoverflow.com/a/43373364/6197439
{
  std::cout << "starting timer" << std::endl;
  timer_started = true;
  tnow = std::chrono::steady_clock::now();
  tend = tnow + std::chrono::milliseconds(interval_ms);
  std::thread([func]()
  {
    std::this_thread::sleep_until(tend);
    func();
  }).detach();
}

void the_function_to_delay(void)
{
  std::cout << "The function (to delay) has ran!" << std::endl;
  do_stop = true;
}

int main()
{
  std::cout << "Press any key to start timed function; press it again to update the timing" << std::endl;
  int kbhit_ret = 0;
  char kbchar;
  while( not(do_stop) )
  {
    kbhit_ret = _kbhit();
    if (kbhit_ret) {
      scanf( "%c", &kbchar ) ;
      std::cout << "kbhit " << kbhit_ret << " " << kbchar << std::endl;
      if (not(timer_started)) {
        timer_start( the_function_to_delay );
      } else {
        update_timing();
      }
    }
    std::cin.clear();
    std::this_thread::sleep_for(std::chrono::milliseconds(1)); //usleep(1000);
  }
}

... in the sense that it always runs the function to delay 3 seconds after the very first keypress, regardless of if there are other key presses in-between.

It kinda makes sense, if std::this_thread::sleep_until(tend); basically "cached" the tend time the very first time it is called.

But if that is the case, how could I get the behavior I want (updateable delay) in C++?

Upvotes: 0

Views: 168

Answers (1)

sdbbs
sdbbs

Reputation: 5384

OK, after some reading, there are in essence two things that prevent the approach in the OP code to work, even if it sort of appears semantically correct:

  1. There is no way to forcibly terminate a single thread in C++ outside of that thread, a thread can be either joined or detached ( see How do I terminate a thread in C++11? ) ; I managed to call the destructor thread_runner.~thread();, but this terminates/aborts all threads, including the main program
  2. Updating the tend variable is not viable either; looking at the sleep_until code from https://gcc.gnu.org/onlinedocs/libstdc++/libstdc++-api-4.5/a01060_source.html#l00265 :
    /// sleep_until
    template<typename _Clock, typename _Duration>
      inline void
      sleep_until(const chrono::time_point<_Clock, _Duration>& __atime)
      { sleep_for(__atime - _Clock::now()); }
    
    ... it basically calculates the delta __atime - _Clock::now() and uses it directly in a call to sleep_for.

Which is to say, we cannot just update timestamps (that is, std::chrono::time_point) and then rely on a single call to sleep_until to obtain the desired behavior.

Instead, we can do our own "waiting loop" where we sleep for a rather small quantum of time (since the desired delay is 3 seconds, 1 ms sleep is acceptable), and then check if the conditions for the equivalent to sleep_until are met, before proceeding to call the delayed function; this short sleep loop will make it fast to wait for a thread to finish, when using .join().

So when a keypress should "update" the timer, we simply set variables and then wait for the delayed caller thread to exit; and then, we can simply restart the timer waiting thread again.

So here is the updated code:

// compile on Linux with:
// g++ test.cpp -o test.exe -pthread

#include <iostream>
#include <chrono>
#include <thread>
#include <functional>

#include <sys/ioctl.h> // FIONREAD
#include <sys/select.h>
#include <termios.h>
#include <stropts.h>

#include <cstdlib>
#include <exception> // std::set_terminate
#include <iostream>

std::chrono::time_point<std::chrono::steady_clock> tnow;
std::chrono::time_point<std::chrono::steady_clock> tstart;
std::chrono::time_point<std::chrono::steady_clock> tend;
unsigned int interval_ms = 3000;
bool do_stop = false;
bool timer_running = false;
bool call_delayed_function = true;

std::thread thread_runner;

int _kbhit() { // https://www.flipcode.com/archives/_kbhit_for_Linux.shtml
  static const int STDIN = 0;
  static bool initialized = false;

  if (! initialized) {
    // Use termios to turn off line buffering
    termios term;
    tcgetattr(STDIN, &term);
    term.c_lflag &= ~ICANON;
    tcsetattr(STDIN, TCSANOW, &term);
    setbuf(stdin, NULL);
    initialized = true;
  }

  int bytesWaiting;
  ioctl(STDIN, FIONREAD, &bytesWaiting);
  return bytesWaiting;
}

char time_buffer[16] = { 0 };
char* get_hms() {
  time_t now = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
  std::strftime(time_buffer, sizeof(time_buffer), "%H:%M:%S ", std::localtime(&now));
  return time_buffer;
}

// https://gcc.gnu.org/onlinedocs/libstdc++/manual/termination.html
// The verbose terminate handler is only available for hosted environments (see Configuring) and will be used by default unless the library is built with --disable-libstdcxx-verbose or with exceptions disabled. If you need to enable it explicitly you can do so by calling the std::set_terminate function.
// check https://en.cppreference.com/w/cpp/error/set_terminate
// note std::set_terminate is a function call, must be called from main;
// if we call it here, we get:
//std::set_terminate([]() //error: expected constructor, destructor, or type conversion before ‘(’ token
//{
//  std::cout << get_hms() << "Terminating" << std::endl;
//});

void the_function_to_delay(void)
{
  std::cout << get_hms() << "The function (to delay) has ran!" << std::endl;
  do_stop = true;
}

void sleeper_threadfunc() {
  //std::this_thread::sleep_until(tend);
  while(timer_running) {
    tnow = std::chrono::steady_clock::now();
    if (tnow >= tend) {
      timer_running = false;
      break; // exit while loop
    }
    std::this_thread::sleep_for(std::chrono::milliseconds(1));
  }
  if (call_delayed_function) {
    the_function_to_delay();
  }
}

void timer_start(std::function<void(void)> func) // inspired by https://stackoverflow.com/a/43373364/6197439
{
  std::cout << get_hms() << "starting timer" << std::endl;
  timer_running = true;
  call_delayed_function = true;
  tstart = std::chrono::steady_clock::now();
  tend = tstart + std::chrono::milliseconds(interval_ms);
  thread_runner = std::thread(sleeper_threadfunc);
}

void update_timing() {
  std::cout << get_hms() << "updating timer" << std::endl;
  //tnow = std::chrono::steady_clock::now();
  //tend = tnow + std::chrono::milliseconds(interval_ms);
  //thread_runner.~thread(); // https://stackoverflow.com/a/12207835/6197439 ; but does not kill the thread if .detach(); has been called; if it works, causes "terminate called without an active exception" written to stderr, which comes from __verbose_terminate_handler in vterminate.cc, e.g. https://github.com/gcc-mirror/gcc/blob/5d2a360/libstdc%2B%2B-v3/libsupc%2B%2B/vterminate.cc#L93 ; however, it terminates ALL threads!
  //timer_start( the_function_to_delay );
  call_delayed_function = false;
  timer_running = false;
  thread_runner.join();
  timer_start( the_function_to_delay );
}


int main()
{
  //std::set_terminate([]()
  //{
  //  std::cout << get_hms() << "Terminating" << std::endl;
  //});
  std::cout << get_hms() << "Press any key to start timed function; press it again to update the timing" << std::endl;
  int kbhit_ret = 0;
  char kbchar;
  while( not(do_stop) )
  {
    kbhit_ret = _kbhit();
    if (kbhit_ret) {
      scanf( "%c", &kbchar ) ;
      std::cout << get_hms() << "kbhit " << kbhit_ret << " " << kbchar << std::endl;
      if (not(timer_running)) {
        timer_start( the_function_to_delay );
      } else {
        update_timing();
      }
    }
    std::cin.clear();
    std::this_thread::sleep_for(std::chrono::milliseconds(1)); //usleep(1000);
  }
  thread_runner.join(); // prevent "terminate called without an active exception" at end
}

... and with a behavior like this:

$ ./test
22:06:42 Press any key to start timed function; press it again to update the timing
d22:06:43 kbhit 1 d
22:06:43 starting timer
d22:06:45 kbhit 1 d
22:06:45 updating timer
22:06:45 starting timer
d22:06:46 kbhit 1 d
22:06:46 updating timer
22:06:46 starting timer
d22:06:47 kbhit 1 d
22:06:47 updating timer
22:06:47 starting timer
d22:06:48 kbhit 1 d
22:06:48 updating timer
22:06:48 starting timer
22:06:51 The function (to delay) has ran!

Upvotes: 0

Related Questions