Search code examples
c++boostboost-asio

Is running new thread in Timer.5 boost::asio tutorial necessary?


Question 1: Is starting a new thread in Timer.5 boost::asio tutorial really needed?

  ...
  boost::thread t(boost::bind(&boost::asio::io_context::run, &io));
  io.run();
  t.join();
  ...

Tutorial says:

As you already know, the asio library provides a guarantee that callback handlers will only be called from threads that are currently calling io_context::run().

But if I run this

  ...
  // boost::thread t(boost::bind(&boost::asio::io_context::run, &io));
  io.run();
  // t.join();
  ...

... callbacks are still executed despite the fact that timers are started asynchronously.

Also I may have understood this tutorial in wrong way, I am new at boost::asio but it looks to me that this tutor doesn't depict what author wanted to demonstrate.

Author says:

The single threaded approach is usually the best place to start when developing applications using asio. The downside is the limitations it places on programs, particularly servers, including:

  • Poor responsiveness when handlers can take a long time to complete.
  • An inability to scale on multiprocessor systems.

So when author tells about poor responsiveness it means that we could run multiple tasks (callbacks in this case) concurrently so that we engage all available cores. But this approach works ideally when there are no shared data to modify. However in this tutorial callbacks DO modify shared variable count and write to cout. To avoid concurrent modification author places callbacks into strand object which guarantees that callbacks execute one after another in order they came into the "queue".

So callbacks are not something heavy and long-to-execute in this tutorial ...

Question 2: Do you think that using strand object is necessary in this tutorial?

I think this tutorial is overengineered, once again please correct me if I am wrong, but this code without additional thread and strand object seems to work the same way as original tutorial:

#include <iostream>
#include <boost/asio.hpp>
#include <boost/thread/thread.hpp>
#include <boost/bind.hpp>

class printer
{
public:
  printer(boost::asio::io_context& io)
    : timer1_(io, boost::asio::chrono::seconds(1)),
      timer2_(io, boost::asio::chrono::seconds(1)),
      count_(0)
  {
    timer1_.async_wait(boost::bind(&printer::print1, this));

    timer2_.async_wait(boost::bind(&printer::print2, this));
  }

  ~printer()
  {
    std::cout << "Final count is " << count_ << std::endl;
  }

  void print1()
  {
    if (count_ < 10)
    {
      std::cout << "Timer 1: " << count_ << std::endl;
      ++count_;

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

  void print2()
  {
    if (count_ < 10)
    {
      std::cout << "Timer 2: " << count_ << std::endl;
      ++count_;

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

private:
  boost::asio::steady_timer timer1_;
  boost::asio::steady_timer timer2_;
  int count_;
};

int main()
{
  boost::asio::io_context io;
  printer p(io);
  io.run();

  return 0;
}

Question 3: Is the code above correct?

Thanks in advance.


Solution

  • Calling run() a from a second thread is the point of the tutorial.

    Your two questions have the same answer. The Timer.5 tutorial shows you how to have more than 1 thread service the io_context (more than 1 thread calls run()). But running multithreaded means that you'd have to protect shared state from concurrent access and that is why the strand object is needed.

    You are right that in this example you don't need the strand if you call run() from only one thread.