c++boostboost-asio

Can i use different methods with different intervals independently using boost::asio::io_context?


I am trying to implement a c++ code which is supposed to call three different methods independent from each other with different intervals. For example let's say method_1 is supposed to be called every T1 seconds and method_2 is supposed to be called every T2 seconds, and finally method3 is supposed to be executed with T3 seconds interval.

Can i implement it like below? Is calling the methods sequentially inside a single thread making them dependent to each other? like assuming the interval of method1 is 100s, and T2 is 1s. isn't calling method1 block calling the method2 every 1s?

Does the is io_context.run() method makes them to be called independent from each other?

enum MethodTypes { FIRST = 0, SECOND = 1, THIRD = 2 };

struct TestTimerContext {
boost::asio::steady_timer timer;
NssStatsTimerContext(boost::asio::io_context& io) : timer(io) {}
};

Assuming all of the following methods are defined inside a class.

start_process (vector<bool> condistions, vector<uint32> intervals) {
    if (conditions[0]) {
       boost::asio::io_context io_context_first;
       start_method (intervals[0], MethodTypes::FIRST, io_context_1);
    }
    if (conditions[1]) {
       boost::asio::io_context io_context_2;
       start_method (intervals[0], MethodTypes::FIRST, io_context_2);
        
    }
    if (conditions[2]) {
       boost::asio::io_context io_context_3;
       start_method (intervals[0], MethodTypes::FIRST, io_context_3);
    }
}


start_method (uint32 interval, MethodTypes method_type, boost::asio::io_context io_context) {
    io_context.run();
    std::unique_ptr<TestTimerContext> ctx;
    ctx           = std::make_unique<TestTimerContext>(io_context);
    auto& ctx_ref = *ctx;
    
    if (interval ==0) {
        func_cb( err, interval, method_type);
    } else {
        
        ctx_ref.timer.expires_after(std::chrono::milliseconds(interval));
        ctx_ref.timer.async_wait([&](const boost::system::error_code& err) mutable {
            func_cb( err, interval, method_type);
        });
    }
}


func_cb (
    const boost::system::error_code& err,
    uint32_t                         interval,
    MethodTypes                         method_type) {

    if (err) {
        cout<<"cb function has an error"<<endl;
        return;
    }

    switch (method_type) {
    case MethodTypes::FIRST:
        method1();
        break;
    case MethodTypes::SECOND:
        methdo2();
        break;
    case MethodTypes::THIRD:
        method3();
        break;
    default:
        cout<<"method type is not supported"<<end;
        return;
    }

    if (interval == 0) {
        // do nothing
    } else {
        auto  ctx     = Arc<TestTimerContext>(io_context_);
        auto& ctx_ref = *ctx;

        ctx_ref.timer.expires_after(std::chrono::milliseconds(interval));
        ctx_ref.timer.async_wait([&](const boost::system::error_code& err) mutable {
            func_cb(err, interval, method_type);
        });
    }
}

UPDATED: This is follow up question:

Thanks @sehe for your comprehensive response and thanks @Christian Stieber for your comments.

I have also confirmed that what @sehe has mentioned i working perfectly. The other question i have is that let's assume trace() method has some dependency and cannot be finalized in 0ms, then it seems like the posted solution will not work properly.

For example if i call trace method like below:

static inline void method_with_delay(std::string const& msg, int delay) {
    std::this_thread::sleep_for(std::chrono::milliseconds(delay));
    static auto const start = now();
    std::cout << std::setw(10) << (now() - start) / 1ms << "ms " << quoted(msg) << std::endl;
}

and assume that we call them like

asio::io_context ioc;
IntervalTimer t1(ioc.get_executor(), 2000ms,    [] { method_with_delay("Method 1", 100); });
IntervalTimer t2(ioc.get_executor(), 1500ms,    [] { method_with_delay("Method 2", 100); });
IntervalTimer t3(ioc.get_executor(), 1200ms,    [] { method_with_delay("Method 3", 300); });

ioc.run_for(6s);

In that case i can see with above approach sometimes it doesn not work perfectly. Also the order of calling them will be important like if the very beginning call has a larger delay the result will get the worst result.

So, following back to what @Christian Stieber has kindly mentioned. I was trying to call them through separate threads like below:

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

namespace asio = boost::asio;
using namespace std::chrono_literals;
using duration            = std::chrono::steady_clock::duration;
static constexpr auto now = std::chrono::steady_clock::now;
using boost::system::error_code;



template <typename Callback> struct IntervalTimer {
    IntervalTimer(asio::any_io_executor ex, duration interval, Callback callback)
        : interval_(interval)
        , cb_(std::move(callback))
        , tim_(ex, interval_) 
    {
        loop();
    }

    IntervalTimer(){};

  private:
    void loop() {
        while (tim_.expiry() <= now())
            tim_.expires_at(tim_.expiry() + interval_);

        tim_.async_wait([this](error_code ec) {
            if (!ec) { // aborted or other failure
                cb_();
                loop();
            }
        });
    }

    duration           interval_;
    Callback           cb_;
    asio::steady_timer tim_;
};

static inline void trace(std::string const& msg) {
    static auto const start = now();
    std::cout << std::setw(10) << (now() - start) / 1ms << "ms " << quoted(msg) << std::endl;
    
}

static inline void method_with_delay(std::string const& msg, int delay, asio::io_context& ioc) {
    std::this_thread::sleep_for(std::chrono::milliseconds(delay));
    static auto const start = now();
    std::cout << std::setw(10) << (now() - start) / 1ms << "ms " << quoted(msg) << std::endl;
    ioc.run();
}


int main() {
    trace("main");
    asio::io_context ioc;
    //boost::asio::io_context& ioc_ref = ioc;
    
    std::thread first_thread = std::thread([&]() {
        IntervalTimer t1 (ioc.get_executor(), 100ms,     [&] { method_with_delay("Method 1", 100, std::ref(ioc)); });
        std::cout << "thread1 is finished"<<std::endl;
        
    });

    std::thread second_thread = std::thread([&]() {
        
         IntervalTimer t2 (ioc.get_executor(), 1500ms,     [&] { method_with_delay("Method 2", 100, std::ref(ioc)); });
        std::cout << "thread-2 is finished"<<std::endl;
        
    });

    std::thread third_thread = std::thread([&]() {
        
        IntervalTimer t3 (ioc.get_executor(), 1200ms,     [&] { method_with_delay("Method 3", 300, std::ref(ioc)); });
        std::cout << "thread-3 is finished"<<std::endl;
        
    });
    
    if(first_thread.joinable())
        first_thread.join();

    if(second_thread.joinable())
        second_thread.join();
    
    if(third_thread.joinable())
        third_thread.join();

    trace("exit");
}

But it only shows me the messages when the thread is done like

         0ms "main"
thread-2 is finished
thread-3 is finished
thread1 is finished
         0ms "exit"

Any hint why the methods cannot be executed in my example?


Solution

  • Yes.

    You would typically use a single io_context (only use seperation in case you need different scheduling e.g. for prioritizing IO over work).

    Since your question code is not self-contained, let me opt to write some simpler code for you instead:

    For example let's say method_1 is supposed to be called every T1 seconds and method_2 is supposed to be called every T2 seconds, and finally method3 is supposed to be executed with T3 seconds interval.

    static inline void trace(std::string const& msg) {
        static auto const start = now();
        std::cout << std::setw(10) << (now() - start) / 1ms << "ms " << quoted(msg) << std::endl;
    }
    
    int main() {
        trace("main");
        asio::io_context ioc;
    
        IntervalTimer t1(ioc.get_executor(), 1s,     [] { trace("Method 1"); });
        IntervalTimer t2(ioc.get_executor(), 1500ms, [] { trace("Method 2"); });
        IntervalTimer t3(ioc.get_executor(), 1200ms, [] { trace("Method 3"); });
    
        ioc.run_for(6s);
        trace("exit");
    }
    

    For the purpose, I've invented IntervalTimer to encapsulate a timer object and the async callback loop:

    template <typename Callback> struct IntervalTimer {
        IntervalTimer(asio::any_io_executor ex, duration interval, Callback callback)
            : interval_(interval)
            , cb_(std::move(callback))
            , tim_(ex, interval_) 
        {
            loop();
        }
    
      private:
        void loop() {
            while (tim_.expiry() <= now())
                tim_.expires_at(tim_.expiry() + interval_);
    
            tim_.async_wait([this](error_code ec) {
                if (!ec) { // aborted or other failure
                    cb_();
                    loop();
                }
            });
        }
    
        duration           interval_;
        Callback           cb_;
        asio::steady_timer tim_;
    };
    

    See it Live On Coliru

    #include <boost/asio.hpp>
    #include <iomanip>
    #include <iostream>
    
    namespace asio = boost::asio;
    using namespace std::chrono_literals;
    using duration            = std::chrono::steady_clock::duration;
    static constexpr auto now = std::chrono::steady_clock::now;
    using boost::system::error_code;
    
    template <typename Callback> struct IntervalTimer {
        IntervalTimer(asio::any_io_executor ex, duration interval, Callback callback)
            : interval_(interval)
            , cb_(std::move(callback))
            , tim_(ex, interval_) 
        {
            loop();
        }
    
      private:
        void loop() {
            while (tim_.expiry() <= now())
                tim_.expires_at(tim_.expiry() + interval_);
    
            tim_.async_wait([this](error_code ec) {
                if (!ec) { // aborted or other failure
                    cb_();
                    loop();
                }
            });
        }
    
        duration           interval_;
        Callback           cb_;
        asio::steady_timer tim_;
    };
    
    static inline void trace(std::string const& msg) {
        static auto const start = now();
        std::cout << std::setw(10) << (now() - start) / 1ms << "ms " << quoted(msg) << std::endl;
    }
    
    int main() {
        trace("main");
        asio::io_context ioc;
    
        IntervalTimer t1(ioc.get_executor(), 1s,     [] { trace("Method 1"); });
        IntervalTimer t2(ioc.get_executor(), 1500ms, [] { trace("Method 2"); });
        IntervalTimer t3(ioc.get_executor(), 1200ms, [] { trace("Method 3"); });
    
        ioc.run_for(6s);
        trace("exit");
    }
    

    Outputting

    g++ -std=c++20 -O2 -Wall -pedantic -pthread main.cpp && ./a.out
             0ms "main"
          1000ms "Method 1"
          1200ms "Method 3"
          1500ms "Method 2"
          2000ms "Method 1"
          2400ms "Method 3"
          3000ms "Method 1"
          3000ms "Method 2"
          3600ms "Method 3"
          4000ms "Method 1"
          4500ms "Method 2"
          4800ms "Method 3"
          5000ms "Method 1"
          6000ms "Method 1"
          6000ms "exit"
    

    With my local interactive demo:

    enter image description here