Search code examples
boostboost-asio

Why do we need to run `io.run` in both main thread and a new thread `t` within asico tutorial Timer.5 example?


Reference: tuttimer5

coliru demo

Based on the tutorial, the main function looks like the following:

int main()
{
  boost::asio::io_context io;
  printer p(io);
  boost::thread t(boost::bind(&boost::asio::io_context::run, &io));
  io.run(); // Why do we need to call io.run() here?
  t.join();

  return 0;
}

I want to understand why we need to have two io.run(); in the main function. Here is my test result:

Test 1 (call io.run() in main thread within main function):

main-Thread #32
printer-Thread #32
print1-Thread #32
        Timer 1: 0
print2-Thread #32
        Timer 2: 1
print1-Thread #32
        Timer 1: 2
print2-Thread #32
        Timer 2: 3
print1-Thread #32
        Timer 1: 4
print2-Thread #32
        Timer 2: 5
print1-Thread #32
        Timer 1: 6
print2-Thread #32
        Timer 2: 7
print1-Thread #32
        Timer 1: 8
print2-Thread #32
        Timer 2: 9
~printer-Thread #32
        Final count is 10

Observation1: both print1 and print2 are called by main-thread.

Test 2 (NOT call io.run() in main thread within main function):

main-Thread #40
printer-Thread #40
print1-Thread #29
        Timer 1: 0
print2-Thread #29
        Timer 2: 1
print1-Thread #29
        Timer 1: 2
print2-Thread #29
        Timer 2: 3
print1-Thread #29
        Timer 1: 4
print2-Thread #29
        Timer 2: 5
print1-Thread #29
        Timer 1: 6
print2-Thread #29
        Timer 2: 7
print1-Thread #29
        Timer 1: 8
print2-Thread #29
        Timer 2: 9
~printer-Thread #40
        Final count is 10

Observation2: Neither print1 and print2 are called by main-thread.

Note: the testing results are come from my local linux box(g++ (Ubuntu 11.1.0-1ubuntu1~18.04.1) 11.1.0)). The result looks a little different from coliru.

In that case io_context operates like a classic thread pool. Asynchronous tasks are performed somewhere on the OS side, however completion handlers are invoked on those threads where io_context::run function is running. To be more precise: every completion handler is invoked on a first free thread which io_context::run function is running on.

Question> what is the main reason that we want to call io.run); in both main and a new thread in the original tutorial?

Thank you


Solution

  • None of it is required. It merely shows that tasks are distributed over all thread participating in running/polling the execution context.

    In that sense, the code as written is simply the cheapest way to get 2 threads running the io context. In a more recent Boost version, one could argue that thread_pool is cleaner code, but results in 3 threads (counting the main thread):

    int main()
    {
        report("main") << std::endl;
        boost::asio::thread_pool io(2);
    
        printer p(io.get_executor());
        io.join();
    }
    

    Live On Coliru

    #include <boost/asio.hpp>
    #include <boost/bind/bind.hpp>
    #include <iomanip>
    #include <iostream>
    #include <thread>
    namespace asio = boost::asio;
    
    std::ostream& report(const std::string& suffix_msg)
    {
        auto tid = std::hash<std::thread::id>{}(std::this_thread::get_id()) % 100;
        return std::cout << suffix_msg << "-Thread #" << std::setw(2)
                         << std::setfill('0') << tid;
    }
    
    class printer {
      public:
        printer(asio::any_io_executor ex)
            : strand_(asio::make_strand(ex))
            , count_(0)
        {
            report("printer") << std::endl;
            timer1_.async_wait(
                std::bind(&printer::print_callback, this, "Timer 1", std::ref(timer1_)));
            timer2_.async_wait(
                std::bind(&printer::print_callback, this, "Timer 2", std::ref(timer2_)));
        }
    
        ~printer()
        {
            report("~printer") << "\tFinal count is " << count_ << std::endl;
        }
    
        void print_callback(std::string_view name, asio::steady_timer& timer)
        {
            if (count_ < 10) {
                report("print_callback") << "\t" << name << ": " << count_ << std::endl;
                ++count_;
    
                timer.expires_at(timer.expiry() + asio::chrono::seconds(1));
                timer.async_wait(std::bind(&printer::print_callback, this, name,
                                           std::ref(timer)));
            }
        }
    
      private:
        asio::any_io_executor strand_;
        int                   count_ = 0;
        asio::steady_timer    timer1_{strand_, asio::chrono::seconds(1)};
        asio::steady_timer    timer2_{strand_, asio::chrono::seconds(1)};
    };
    
    int main()
    {
        report("main") << std::endl;
        boost::asio::thread_pool io(2);
    
        printer p(io.get_executor());
        io.join();
    }
    

    Prints e.g.

    main-Thread #59
    printer-Thread #59
    print_callback-Thread #06   Timer 1: 0
    print_callback-Thread #06   Timer 2: 1
    print_callback-Thread #74   Timer 1: 2
    print_callback-Thread #74   Timer 2: 3
    print_callback-Thread #74   Timer 1: 4
    print_callback-Thread #74   Timer 2: 5
    print_callback-Thread #74   Timer 1: 6
    print_callback-Thread #74   Timer 2: 7
    print_callback-Thread #74   Timer 1: 8
    print_callback-Thread #74   Timer 2: 9
    ~printer-Thread #59 Final count is 10