Modern C++ Concurrency - How to use a thread object correctly and common pitfalls

tutorial3

Modern C++ Concurrency - How to use a thread object correctly.

This lesson is going to be more theory focused because we will covers some important fact about how to correctly use the thread object.
For instance we will be talking about how to

  1. Pass around threads
  2. Have side effect on object passed to thread by reference
  3. How to avoid common undefined reference situations
  4. How to identify threads uniquely by an id.

How to pass arguments to a thread function

As we have seen in the previous tutorials (helloworld, vector sum 1 and vector sum 2) providing arguments to the functor (callable object) is as simple as listing them as additional parameter to the thread constructor.

The way this work is very much likely the std::bind, function works. In-fact thread‘s constructor and bind function signature looks remarkably similar as shown in the following listing:

template< class F, class... Args >
/*unspecified*/ bind( F&& f, Args&&... args );

template< class Function, class... Args >
explicit thread( Function&& f, Args&&... args );

The first thing it should always be remembered when dealing with thread is that the additional parameters are always copied into the newly created thread even if the Function takes them as reference.
For instance the following code does not even compile.

void function(string& str){
  std::reverse(str.begin(), str.end());
}

int main(){
  std::string string_main("I am not modified");
  thread t1(function, string_main);
  t1.join();
  return 0;
}

gcc 8.1.2 returns a quite long error.

/usr/include/c++/8.2.1/thread: In instantiation of ‘std::thread::thread(_Callable&&, _Args&& ...) [with _Callable = void (&)(std::__cxx11::basic_string<char>&); _Args = {std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&}; <template-parameter-1-3> = void]’:
tutorial3.cpp:22:34:   required from here
/usr/include/c++/8.2.1/thread:120:17: error: static assertion failed: std::thread arguments must be invocable after conversion to rvalues
  static_assert( __is_invocable<typename decay<_Callable>::type,
                 ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
           typename decay<_Args>::type...>::value,
           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/usr/include/c++/8.2.1/thread: In instantiation of ‘struct std::thread::_Invoker<std::tuple<void (*)(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&), std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > >’:
/usr/include/c++/8.2.1/thread:132:22:   required from ‘std::thread::thread(_Callable&&, _Args&& ...) [with _Callable = void (&)(std::__cxx11::basic_string<char>&); _Args = {std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&}; <template-parameter-1-3> = void]’
tutorial3.cpp:22:34:   required from here
/usr/include/c++/8.2.1/thread:250:2: error: no matching function for call to ‘std::thread::_Invoker<std::tuple<void (*)(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&), std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > >::_M_invoke(std::thread::_Invoker<std::tuple<void (*)(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&), std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > >::_Indices)’
  operator()()

If you look closely you can see that it gives a clear explanation of the reason.

static_assert failed due to requirement…
std::thread arguments must be invocable after conversion to rvalues

Why? This happens because the following assertion is failing.

static_assert(
  __is_invocable<typename decay<void (&)(basic_string<char...> &)>::type, typename decay<basic_string<char...> &>::type>::value
  )

Let’s break this down and understand what the compiler is trying to do:

  1. decay convert string& in a plain string: if you try this on your compiler you will see that the following return true; is_same< decay<string&>::type , string>::value
  2. is_invocable checks whether it’s first parameter i.e. the function function can be invoked when the supplied argument are not evaluated! That is the key part. The plain string does not get the chance to be implicitly converted to string&

This can be simply checked by trying to compile the following

int main()
{
    static_assert( 
      std::is_invocable<
        void(string&),
        std::string>::value 
      );
}

std::is_invocable return false in this case. This is exactly what is happening here.
According to is_invocable you cannot call a function taking a reference to a string by passing a plain string.

You can easily solve this by always wrapping the variable the thread’s function is expecting as a reference in a std::ref as shown in the listing below.
This code also behave as expected, i.e. string_mainis modified by the thread and the side effect can be seen in the main thread.

void function(string& str){
  std::reverse(str.begin(), str.end());
}

int main(){
  std::string string_main("I am not modified");
  thread t1(function, std::ref(string_main)); //std::ref
  t1.join();
  return 0;
}
(18:05:38)[knotman@archazzo] 
→ g++ -O3 -std=c++17  -Wall  -Wextra -pthread -o tutorial3  tutorial3.cpp 

(18:05:50)[knotman@archazzo] 
→ ./tutorial3
Before: I am not modified
In thread function after reverse: deifidom ton ma I
After: deifidom ton ma I

Undefined reference when passing reference or pointers

Another common pitfall involves passing reference parameters to the thread’s function. We need to be absolutely sure that the variable we are passing as a reference does not go out of scope while the threads holds a reference to it.

The following code illustrates the problem:

int function(int& num){
  //num might be a dangling reference
    for(int i = 0; i < 500; i++)
      num += 1;
    return num;
}

thread fn(){
  int num = 10;
  //std::ref
  thread t1(function, std::ref(num)); 
  return t1;
  //at this point `t1` might
  //still be running and using `num`
}
int main(){
  thread t1 = fn();
  //t1 holds an invalid reference at this point.
  t1.join();
  return 0;
}

The problem with the code above is that t1 in the main function holdd a reference to the local variable num in fn. When fn returns num will be invalidate as it get out of scope but t1 might be still using it afterwards if it does not complete before fn returns.

Passing threads around

Note that like uniq_ptr, thread is not copyable. So whenever you need to transfer the ownership of the thread from one function to the other, like we did in the example above, from fn to main, we have to std::move the thread instance.
When you return a thread from a function, move happens implicitly without need to a call to move because t1 in fn is a temporary object and the move constructor is defined in thread (see listing below) (check this link if you need a refresher on move operation.).

//move constructor
thread( thread&& other );
//not copyable
thread(const thread&) = delete

An easy way to visualize what happen when you move a thread is by looking at the following snippet of code.

extern void f1();
int main(){
  thread t1(f1);
  thread t2 = t1;
  //at this point t1 is invalid
  return 0;
}

When t2 is assigned, t1 is not valid anymore. The ownership of the thread owned originated from t1 is moved to t2.

Identifying threads

Threads are assigned with a unique identifier of type std::thread::id. The id can be retrieved using the member function get_id().
The id serves the purpose of identifying uniquely a thread, but it cannot be used as an index into a structure. It is not guaranteed by the standard what is the real type used by std::thread::id. In a nutshell, it is not guaranteed to be a number. The only guarantee that you have is that you can compare them and whenever two variables of type std::thread::id are equal, it means that the thread they have been retrieved from is the same.

std::thread::id id_of_master;
void do_stuff(){
  //all threads perform these operations

  if(std::this_thread::get_id() == id_of_master){
    //only the master thread will execute this
  }

}
int main(){
  id_of_master = std::this_thread::get_id();

  thread child1(do_stuff);
  thread child2(do_stuff);

  //master thread execute
  do_stuff();
  master.join();
}

References

cppreference.com, std::move

Be the first to leave a comment. Don’t be shy.

Join the Discussion

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>