Removing Safe Timer

I am trying to cancel boost::asio::basic_waitable_timer<std::chrono::steady_clock> safely.

According to this answer, this code should do the job:

 timer.get_io_service().post([&]{timer.cancel();}) 

I am afraid this will not work for me.
Am I something wrong? This is my code:

 #include <iostream> #include "boost/asio.hpp" #include <chrono> #include <thread> #include <random> boost::asio::io_service io_service; boost::asio::basic_waitable_timer<std::chrono::steady_clock> timer(io_service); std::atomic<bool> started; void handle_timeout(const boost::system::error_code& ec) { if (!ec) { started = true; std::cerr << "tid: " << std::this_thread::get_id() << ", handle_timeout\n"; timer.expires_from_now(std::chrono::milliseconds(10)); timer.async_wait(&handle_timeout); } else if (ec == boost::asio::error::operation_aborted) { std::cerr << "tid: " << std::this_thread::get_id() << ", handle_timeout aborted\n"; } else { std::cerr << "tid: " << std::this_thread::get_id() << ", handle_timeout another error\n"; } } int main() { std::cout << "tid: " << std::this_thread::get_id() << ", Hello, World!" << std::endl; std::random_device rd; std::mt19937 gen(rd()); std::uniform_int_distribution<> dis(1, 100); for (auto i = 0; i < 1000; i++) { started = false; std::thread t([&](){ timer.expires_from_now(std::chrono::milliseconds(0)); timer.async_wait(&handle_timeout); io_service.run(); }); while (!started) {}; auto sleep = dis(gen); std::cout << "tid: " << std::this_thread::get_id() << ", i: " << i << ", sleeps for " << sleep << " [ms]" << std::endl; std::this_thread::sleep_for(std::chrono::milliseconds(sleep)); timer.get_io_service().post([](){ std::cerr << "tid: " << std::this_thread::get_id() << ", cancelling in post\n"; timer.cancel(); }); // timer.cancel(); std::cout << "tid: " << std::this_thread::get_id() << ", i: " << i << ", waiting for thread to join()" << std::endl; t.join(); io_service.reset(); } return 0; } 

This is the conclusion:

...
tid: 140737335076608, handle_timeout
tid: 140737335076608, handle_timeout
tid: 140737353967488, i: 2, wait for thread to join ()
tid: 140737335076608, cancellation of office
tid: 140737335076608, handle_timeout canceled
tid: 140737353967488, i: 3, sleeps for 21 [ms]
tid: 140737335076608, handle_timeout
tid: 140737353967488, i: 3, wait for thread to join ()
tid: 140737335076608, handle_timeout
tid: 140737335076608, cancellation of office
tid: 140737335076608, handle_timeout
tid: 140737335076608, handle_timeout
tid: 140737335076608, handle_timeout
tid: 140737335076608, handle_timeout
tid: 140737335076608, handle_timeout
...
continue forever ...

As you can see, timer.cancel() is called from the corresponding thread:

tid: 140737335076608, cancel in message

But no

tid: 140737335076608, handle_timeout aborted

Further.

Main is always waiting.

+5
source share
1 answer

Cancel safe.

This is just not great. You did not consider the case when the timer was not delayed. Then you cancel it once, but it just starts a new asynchronous call after calling the completion handler.

Listed below are my detailed steps for tracking the issue.

SUMMARY TL; DR

Canceling time only cancels asynchronous operations in flight.

If you want to disable the asynchronous call chain, you will have to use additional logic for this. An example is given below.

Handler Tracking

Turn on with

 #define BOOST_ASIO_ENABLE_HANDLER_TRACKING 1 

This creates output that can be visualized using boost/libs/asio/tools/handlerviz.pl :

Successful trace

enter image description here

As you can see, async_wait is in flight when cancellation occurs.

Bad trace

(truncated because it will work endlessly)

enter image description here

Notice how the completion handler sees cc=system:0 , not cc=system:125 (for operation_aborted ). This is a sign of the fact that the published cancellation did not actually “take”. The only logical explanation (not visible in the diagram) is that the timer has already expired before the cancel request is called.

Compare unpaved tracks¹

enter image description here

¹ removing noisy difference

Detection

So we have an advantage. Can we find him?

  timer.get_io_service().post([](){ std::cerr << "tid: " << std::this_thread::get_id() << ", cancelling in post\n"; if (timer.expires_from_now() >= std::chrono::steady_clock::duration(0)) { timer.cancel(); } else { std::cout << "PANIC\n"; timer.cancel(); } }); 

Print

 tid: 140113177143232, i: 0, waiting for thread to join() tid: 140113177143232, i: 1, waiting for thread to join() tid: 140113177143232, i: 2, waiting for thread to join() tid: 140113177143232, i: 3, waiting for thread to join() tid: 140113177143232, i: 4, waiting for thread to join() tid: 140113177143232, i: 5, waiting for thread to join() tid: 140113177143232, i: 6, waiting for thread to join() tid: 140113177143232, i: 7, waiting for thread to join() tid: 140113177143232, i: 8, waiting for thread to join() tid: 140113177143232, i: 9, waiting for thread to join() tid: 140113177143232, i: 10, waiting for thread to join() tid: 140113177143232, i: 11, waiting for thread to join() tid: 140113177143232, i: 12, waiting for thread to join() tid: 140113177143232, i: 13, waiting for thread to join() tid: 140113177143232, i: 14, waiting for thread to join() tid: 140113177143232, i: 15, waiting for thread to join() tid: 140113177143232, i: 16, waiting for thread to join() tid: 140113177143232, i: 17, waiting for thread to join() tid: 140113177143232, i: 18, waiting for thread to join() tid: 140113177143232, i: 19, waiting for thread to join() tid: 140113177143232, i: 20, waiting for thread to join() tid: 140113177143232, i: 21, waiting for thread to join() tid: 140113177143232, i: 22, waiting for thread to join() tid: 140113177143232, i: 23, waiting for thread to join() tid: 140113177143232, i: 24, waiting for thread to join() tid: 140113177143232, i: 25, waiting for thread to join() tid: 140113177143232, i: 26, waiting for thread to join() PANIC 

Can we communicate super-cancellation in a different, clearer way? We have ... only a timer object to work with, of course:

Alarm off

The timer object does not have many properties to work with. There are no close() or similar ones, for example, in a socket, which can be used to send a timer to some invalid state.

However, there is an expiration point, and we can use a special value domain for the signal "invalid" for our application:

 timer.get_io_service().post([](){ std::cerr << "tid: " << std::this_thread::get_id() << ", cancelling in post\n"; // also cancels: timer.expires_at(Timer::clock_type::time_point::min()); }); 

This "special value" is easily handled in the completion handler:

 void handle_timeout(const boost::system::error_code& ec) { if (!ec) { started = true; if (timer.expires_at() != Timer::time_point::min()) { timer.expires_from_now(std::chrono::milliseconds(10)); timer.async_wait(&handle_timeout); } else { std::cerr << "handle_timeout: detected shutdown\n"; } } else if (ec != boost::asio::error::operation_aborted) { std::cerr << "tid: " << std::this_thread::get_id() << ", handle_timeout error " << ec.message() << "\n"; } } 
+6
source

Source: https://habr.com/ru/post/1266228/


All Articles