Reputation: 5384
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
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:
thread_runner.~thread();
, but this terminates/aborts all threads, including the main programtend
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