c++boostboost-asioasio

how to run 2 asynchronous timers implemented in a class


I want the second timer to start working half a second after the second one, but the moment the second timer starts working, the terminal displays something completely different from what I want. If it does not impose restrictions before executing the second timer, then they both work as they should

#include <iostream>
#include <functional>
#include <boost/asio.hpp>

class print{
public:
    print(boost::asio::io_context& io) : timer_(io, boost::asio::chrono::seconds(1)), count_(0){
        timer_.async_wait(std::bind(&print::printer, this));
    }

    void printer(){
        if(count_ < 5){
            std::cout << count_ << "\n";
            ++count_;

            timer_.expires_at(timer_.expiry() + boost::asio::chrono::seconds(1));
            timer_.async_wait(std::bind(&print::printer, this));
        }
    }

    ~print(){
        std::cout << "Last num " << count_ << "\n";
    }
private:
    boost::asio::steady_timer timer_;
    int count_;
};

int main(){
    boost::asio::io_context io;
    
    print p(io);

    boost::asio::steady_timer t(io, boost::asio::chrono::seconds(2));
    t.async_wait([&io](const boost::system::error_code& ec){
        if(!ec){
            print pp(io);
        }
    });

    io.run();   
}

that's what the terminal outputs

0,
1,
Last num 0,
2,
3,
4,
Last num 5.

Solution

  • The second timer pp is a local variable. It is destructed immediately.

    Therefore the async operation is using a deleted instance and invoked Undefined Behaviour.

    You should probably be detecting cancellation by actually receiving error_code in the completion handler. The std::bind expression silently drops it for you, now.

    You can use dynamic allocation of some sort. I also strongly suggest separating and generalizing the notion of an interval timer and the action that is executed:

    struct Timer {
        using Callback = std::function<bool()>;
    
        Timer(asio::any_io_executor ex, duration d, Callback cb)
            : timer_(ex, d)
            , interval_(d)
            , cb_(std::move(cb)) { do_loop(); }
    
      private:
        asio::steady_timer timer_;
        duration           interval_;
        Callback           cb_;
    
        void do_loop() {
            timer_.async_wait(bind(&Timer::on_tick, this, std::placeholders::_1));
        }
    
        void on_tick(error_code ec) {
            if (ec) {
                std::cerr << "Warning: " << ec.message() << std::endl;
                return;
            } else if (cb_()) {
                timer_.expires_at(timer_.expiry() + interval_);
                do_loop();
            }
        }
    };
    

    Now you can use it like:

    #include <boost/asio.hpp>
    #include <functional>
    #include <iostream>
    namespace asio = boost::asio;
    using namespace std::chrono_literals;
    using boost::system::error_code;
    using duration = std::chrono::steady_clock::duration;
    
    struct Timer {
        using Callback = std::function<bool()>;
    
        Timer(asio::any_io_executor ex, duration d, Callback cb)
            : timer_(ex, d)
            , interval_(d)
            , cb_(std::move(cb)) { do_loop(); }
    
      private:
        asio::steady_timer timer_;
        duration           interval_;
        Callback           cb_;
    
        void do_loop() {
            timer_.async_wait(bind(&Timer::on_tick, this, std::placeholders::_1));
        }
    
        void on_tick(error_code ec) {
            if (ec) {
                std::cerr << "Warning: " << ec.message() << std::endl;
                return;
            } else if (cb_()) {
                timer_.expires_at(timer_.expiry() + interval_);
                do_loop();
            }
        }
    };
    
    int main() {
        asio::io_context io;
        auto ex = io.get_executor();
    
        auto printer = [](std::string_view name) {
            return [name, count_ = 0]() mutable {
                bool more = count_ < 5;
                std::cout << "Printer " << quoted(name) << (more ? " " : " Last num ") << count_ << "\n";
                ++count_;
                return more;
            };
        };
    
        std::optional<Timer> second;
    
        Timer first(ex, 1s, printer("First"));
        Timer delay(ex, 1500ms, [&] {
            second.emplace(ex, 1s, printer("Second"));
            return false;
        });
    
        io.run();
    }
    

    See it Live On Coliru

    Which prints:

    Printer "First" 0
    Printer "First" 1
    Printer "Second" 0
    Printer "First" 2
    Printer "Second" 1
    Printer "First" 3
    Printer "Second" 2
    Printer "First" 4
    Printer "Second" 3
    Printer "First" Last num 5
    Printer "Second" 4
    Printer "Second" Last num 5
    

    Or more interactively:

    enter image description here