Running a few boost::thread's

Conceptually speaking, running a boost::thread is not a big deal. We create a thread object passing to it the piece of code we want to run for it, and that's it. The boost::thread object takes care of the entire life span of the actual thread.

But let's give some more details on how we could define the code that we want to run, having a look to a simple example that uses an old plain free function, member functions, static and not, and a functor.

Getting feedback

The code I am about to write is quite simple, but still it make sense to have an utility function that dumps to standard output a timestamp and a message. And since we are in a multithreaded world, we should pay attention to carefully shield by mutex the access to shared resources:
boost::mutex mio; // 1
void print(int flag, const char* msg)
{
  boost::lock_guard<boost::mutex> lock(mio); // 2
  std::cout << boost::posix_time::microsec_clock::universal_time() << ' ' // 3
    << flag << ": " << msg << " " << boost::this_thread::get_id() << std::endl;
}
1. Mutex used for avoid clashes when writing to the standard output console.
2. We achieve a simple RAII for acquiring and releasing a mutex through lock_guard.
3. This Boost Date_Time function gives us the current time in a portable way.

Free function

Here is a plain free function that we want to run in a separate thread. Notice that we call yield(), function available from each thread, even if not associated to a boost::thread object, at the end of any loop cycle, so that we notify to the system that the current thread could temporary stop running, if required to:
void runner(int times)
{
  for(int i=0; i < times; ++i)
  {
    print(i, "runner");
    boost::this_thread::yield();
  }
}
We can create a new thread running that free function in this way:
const int TIMES = 10;
boost::thread t1(runner, TIMES);
The first argument we pass to the ctor is the function address we want to run, then we pass all the arguments required by the function itself.

Static member function

There is no "this" pointer for a static class member, that is just included in the class namespace. So we don't expect a big difference between using free functions and static member functions in a Boost Thread context.

Considering this class:
class ARunner
{
public:
  static void runner(int times);
};

void ARunner::runner(int times)
{
  for(int i=0; i < times; ++i)
  {
    print(i, "static runner");
    boost::this_thread::yield();
  }
}
We can run a thread on its static member function like this:
boost::thread t2(ARunner::runner, TIMES);
We simply have to specify the fully qualified function name, so that the compiler could find it.

Functor

A functor is nothing more than a plain class for which is defined the operator(), so that an instance of that class could be used where a functional parameter is expected. Here is an example of such a beast:
class FunRunner : public boost::noncopyable // 1
{
private:
  unsigned const int TIMES_;
public:
  FunRunner(unsigned int times) : TIMES_(times) {}
  void operator()()
  {
    for(unsigned int i=0; i < TIMES_; ++i)
    {
      print(i, "functor runner");
      boost::this_thread::yield();
    }
  }
};
The ctor set an internal constant, and the operator() uses it in its looping.

Here is the code that uses our functor to create and run a new thread:
FunRunner fr(TIMES);
boost::thread t3(std::ref(fr));
Notice that we passed the functor by reference to the boost::thread constructor, using the std::ref() wrapper. If we won't do that, we would need a useless copy of the functor just for the boost::thread object internal usage. But we designed the functor to be non-copyable, so the code wouldn't compile at all.

Member function

Let's add a normal member function to the class we just created:
class FunRunner : public boost::noncopyable
{
// ...
public:
  void runner(const char* msg)
  {
    for(unsigned int i=0; i < TIMES_; ++i)
    {
      print(i, msg);
      boost::this_thread::yield();
    }
  }
// ...
};
Creating a boost::thread that runs this runner() function is a tad more complicated than the other ways:
boost::thread t4(&FunRunner::runner, &fr, "generic member function runner");
The constructor needs to know the function name, then it needs the "this" pointer to the class object, and finally all the parameters required from the function we want to run in the new thread.

Yielding and joining in main()

As we have seen, yield() is a free function that could be called from any thread, so we can call it from the master thread, in our main() function:
for(int i=0; i < TIMES; ++i)
{
  print(i, "main");
  boost::this_thread::yield();
}
In this sense, there is no difference among the different threads, we ask the system to run them, or we give it an hint that we could stop running for a while, but it is the operating system that decides what it is actually and where - if our application is running on a multiprocessor / multicore hardware.

The difference is that the thread that creates other threads should wait till the end of the job of its spawn before returning, and this is done by calling join() on the boost::thread objects:
t1.join();
t2.join();
t3.join();
t4.join();
This list on joins looks a bit funny, we could have made it more professional using a boost::thread_group, but we'll see it in a next post.

No comments:

Post a Comment