C++ Reactive Programming
上QQ阅读APP看书,第一时间看更新

Ownership management

From the examples discussed so far in this chapter, you might have noticed that the function that launches the thread has to wait for the thread to complete its execution using the join() function, otherwise it will call detach() with a cost of the program losing control over the thread. In modern C++, many standard types are movable, but cannot be copied; std::thread is one of them. This means that the ownership of a thread's execution can be moved between std::thread instances with the help of move semantics.

There are many situations where we want to move the ownership to another thread, for example, if we want the thread to run in the background without waiting for it on the function that created the thread. This can be achieved by passing the thread ownership to a calling function rather than waiting for it to complete in the created function. In another instance, pass the ownership to some other function, which will wait for the thread to complete its execution. Both of these cases can be achieved by passing the ownership from one thread instance to another.

To explain further, let us define two functions to use as the thread functions:

void function1() 
{ 
    std::cout << "function1()n"; 
} 
 
void function2() 
{ 
    std::cout << "function2()n"; 
} 

Let's look into the main function that spawns threads from previously declared functions:

int main() 
{ 
    std::thread t1(function1); 
     
    // Ownership of t1 is transferred to t2 
    std::thread t2 = std::move(t1);

In the preceding code, a new thread started with t1 in the first line of main(). Ownership is then transferred to t2 using the std::move() function, which is invoking the move constructor of std::thread, which is associated with t2. Now, the t1 instance has no associated thread of execution. The initialization function function1() is now associated with t2:

    t1 = std::thread(function2); 

Then, a new thread is started using an rvalue, which invokes the move assignment operator of std::thread, which is associated with t1. Since we are using an rvalue, an explicit call to std::move() is not required:

    // thread instance Created without any associated thread execution 
    std::thread t3; 
     
    // Ownership of t2 is transferred to t3 
    t3 = std::move(t2); 

t3 was instantiated without any thread of execution, which means it is invoking the default constructor. The ownership currently associated with t2 is then transferred to t3 by the move assignment operator, by explicitly calling the std::move() function:

    // No need to join t1, no longer has any associated thread of execution 
    if (t1.joinable())  t1.join(); 
    if (t3.joinable())  t3.join(); 
     
    return 0; 
} 

Finally, the std::thread instances with an associated thread of execution are joined before the program exits. Here, t1 and t3 are the instances with an associated thread of execution.

Now, let's assume that the following code is present before the threads join() in the preceding example:

t1 = std::move(t3); 

Here, the instance t1 is already associated with a running function (function2). When std::move() attempts to transfer the ownership of function1 back to t1, std::terminate() is called to terminate the program. This guarantees the consistency of the std::thread destructor.

The move support in std::thread helps in transferring the ownership of a thread out of a function. The following example demonstrates such a scenario:

void func() 
{ 
    std::cout << "func()n"; 
} 
 
std::thread thread_creator() 
{ 
    return std::thread(func); 
} 
 
void thread_wait_func() 
{ 
    std::thread t = thread_creator(); 
     
    t.join(); 
} 

Here, the thread_creator() function returns the std::thread associated with the func() function. The thread_wait_func() function calls thread_creator(), and then returns the thread object, which is an rvalue that is assigned to an std::thread object. This transfers the ownership of the thread into the std::thread object t, and object t is waiting for the completion of thread execution in the transferred function.