Tuesday, July 2, 2013

Passing arguments to std::thread

The C++11 standard library has a powerful mecanism for managing threads: class std::thread, which follows the principle of providing a simple interface for doing simple things.

Let's start with a very simple and useless example:


void f() {
  do_some_work();
}

void g() {
  do_some_more_work();
}

void h() {
  std::thread t1{f}; // Launch f in a thread
  std::thread t2{g}; // Launch g in a thread

  t1.join(); // Blocks until t1 finishes
  t2.join(); // Blocks until t2 finishes
}

I say it is useless, beause for most uses of concurrency one needs to send arguments to the threads. However it is simple to follow. Function h() launches two threads: t1 and t2. Thread t1 runs function f() and thread t2 runs function g().

Now let's add to our example arguments to be passed to our threads.

void f(int n) {
  for (int i=0; i<n; ++i) {
    do_some_work();
  }
}

void g(int a, int b) {
  for (int i=a; i<=b; ++i) {
    do_some_more_work();
  }
}

void h() {
  std::thread t1{f, 10}; // Launch f(10) in a thread
  std::thread t2{g, 15, 25}; // Launch g(15,25) in a thread

  t1.join(); // Blocks until t1 finishes
  t2.join(); // Blocks until t2 finishes
}

Now, we can launch threads with function calls to functions with any number of arguments in a simple way. At this point, you may remember how long you spent doing type casts with your old good pthreads or similar API.

Looks wonderful, isn't it? Well, now is when the tricky part comes into scene.

What is the real signature of std::thread constructor?

template <typename F, typename ... Args> 
explicit thread(F&& f, Args&&... args);

What area this ellipsis in this template constructor? This is a variadic template, which is a C++11 mechanism to provide a template with a variable number of template arguments. You can see this declaration as the following family of declarations:

template <typename F, typename A1> 
explicit thread(F&& f, A1&& a1);

template <typename F, typename A1, typename A2> 
explicit thread(F&& f, A1&& a1, A2&& a2);

template <typename F, typename A1, typename A2, typename A3> 
explicit thread(F&& f, A1&& a1, A2&& a2, A3&& a3);

And so on.

If you are not (still) quite used to C++11, you may be thinking about what are those double ampersands. You probably already know that, in C++ there is no reference-to-reference. So, what is this? In general, a double ampersand denotes an r-value reference. However, in combination with templates they have some special rules. Scott Meyers named this combination a universal reference. In practice, this means that if you pass an l-value (e.g. a variable) you ar passing by reference, but if you pass an r-value (e.g. an expression) you are passing by value.

With this in mind, we can pass variables to a thread:

void f(int n) {
  for (int i=0; i<n; ++i) {
    do_some_work();
  }
}

void g(int a, int b) {
  for (int i=a; i<=b; ++i) {
    do_some_more_work();
  }
}

void h() {
  int x = 10, y = 15, z = 25;
  std::thread t1{f, x}; // Launch f(x) in a thread
  std::thread t2{g, y, z}; // Launch g(y,z) in a thread

  t1.join(); // Blocks until t1 finishes
  t2.join(); // Blocks until t2 finishes
}

Then. Can I use functions tanking arguments by reference? Well, yes and no. I mean, it is not so easy.

Let's start with a broken example:

void f(int & n) {
  ++n;
}

void g() {
  int x =10;
  std::thread t1(f, x);
  t1.join();
  std::cout << x << std::endl;
}

If you compile that piece of code, you may find that you get a compiler error. What is the problem?

The standard requires that a copy of your parameter is passed to the new thread. Thus it will only work with parameters either by value or by const reference, but not by reference.

Magically, the solution comes with std::ref() (and its colleague std::cref). Both are means for making a reference_wrapper from a variable.

void f(int & n) {
  ++n;
}

void g() {
  int x =10;
  std::thread t1(f, std::ref(x));
  t1.join();
  std::cout << x << std::endl;
}

So, remember it. If you want to pass something by reference to a thread, you need to use std::ref(). If you want to pass something by const reference to a thread, you need to use std::cref().

However, in most cases there are better approaches than passing references to threads. Please, think if you absolutely need to pass a reference to a thread before doing so. They could be a source for data races or lead to strange dangling references if the referenced value happens to be destroyed. In summary, you should be very sure of what you are doing before passing by reference to a thread.

Next time, I will try to write something about returning values frome threads.