Wednesday 22 February 2017

C++11 Features - std::thread

1. Creation
To create a thread in C++11 is to create a std::thread object by passing a function and its argument(s). The passing function can be a standalone function, functor, member function or lambda function. And the arguments are following the function as the extra arguments passed into std::thread.

Create std::thread object:
* Standalone Function
A standalone function is the first argument of std::thread and its arguments follow on as the 2nd, 3rd ... arguments of std::thread.

/*****************************************/
    void Foo(int x, double y) {}
    int x = 1;
    double y = 2.0;
    std::thread t1(Foo, x, y);
    std::thread t2(Foo, 1, 2.0);
/*****************************************/

* Functor
Those programmers work on STL algorithms and iterator should not be unfamiliar with functor. It refers to a callable object. Literally it is a class with overloaded implementation of operator ().

/*****************************************/
    struct Bar {
        void operator() (int x, double y)
        {}
    };

    Bar b;
    int x = 1;
    double y = 2.0;

    std::thread t3(b, x, y);
    std::thread t4(Bar(), 1, 2.0);
/*****************************************/

Most-Vexing Problem in Functor
It is interpreted as a function declaration to take a function pointer that takes no arguments and returns an object Bar as parameter and to return an object of std::thread

The first fix is to use brackets to enforce the compiler to evaluate "Bar()" first to get an object of Bar, then passed as an argument to std::thread.
The second fix is to use initialization list of std::thread to tell compiler explicitly to create/initialize std::thread object.
/*****************************************/
    struct Bar {
        void operator() ()
        {}
    };

    std::thread(Bar()); // most-vexing problem
    std::thread((Bar())); // fix
    std::thread{Bar()}; // fix
/*****************************************/

* Member Function
The member function of a class type can be passed as the first argument of std::thread. And the second argument of std::thread has to be one of the following,
    - an instance/object of this class type
    - a reference to an instance/object of this class type
    - a pointer to an instance/object of this class type

/*****************************************/
    struct Bar {
        void DoWork(int x, double y)
        {}
    };

    Bar b;
    int x = 1;
    double y = 2.0;

    std::thread t1(&B:DoWork, b, x, y);
    std::thread t2(&B:DoWork, std::ref(b), x, y);
    std::thread t3(&B:DoWork, &b, x, y);
/*****************************************/

* Lambda Function
std::thread can take a lambda function as the first argument. And the lambda's catch defines what has been passed, copy or reference.

/*****************************************/
    struct Bar {
        void DoWork(int x, double y)
        {}
    };

    Bar b;
    int x = 1;
    double y = 2.0;

    std::thread t1([=](){b.DoWork(x, y);});
    std::thread t2([&](){b.DoWork(x, y);});
/*****************************************/

Passing Arugments:
By default std::thread is to make a copy of the passing arguments in the parent thread. Then move the copy into the child thread. Be particular careful about passing by reference. In order to make sure the reference is passed to the function associated with the child thread, std::ref is to be used.

/*****************************************/
    void Foo(int x, double& y)
   {
        y += x;
   }
    int x = 1;
    double y = 2.0;
    std::thread t1(Foo, x, y); // y is not updated after Foo() returned
    std::thread t1(Foo, x, std::ref(y)); // y is updated as expected
/*****************************************/

For something like a functor has states, then std::ref() or a pointer to the functor is required to pass into std::thread(). Otherwise the object will be copied and the state will only reflected on the newly-copied object in std::thread constrcutor.
/*****************************************/
    struct Bar {
        int x;
        Bar() : x(0)
        {}

        void Increment() {
            ++x;
        }
    };

    Bar b;
   
    std::thread t1(&B:DoWork, b); // b.x = 0
    std::thread t2(&B:DoWork, std::ref(b)); // b.x incremented by 1
    std::thread t3(&B:DoWork, &b, x, y); // b.x incremented by 1
/*****************************************/

As I mentioned above, the reference (via std::ref) or a pointer can be passed into the function associated with the child thread, then it is programmer's responsibility that make sure the passed references or pointers are not referring/pointing to objects which are out-of-life. This causes dangling pointer/reference pointer and causes crashes.

2. Destruction
C++11 standard says that std::terminate() is to be called and therefore causes the crash of the program, when std::thread object is destroyed before neither it is an empty object nor it becomes un-joinable. std::thread object has to call std::thread::join() or std::thread::detach() upon itself in order to become un-joinable. Call std::thread::joinable() to query its status.

Join
std::thread::join() is a very strong statement. Literally it says that the caller thread will block itself until the callee thread terminates - becoming un-joinable.
std::thread::join() is the way usually often used to make sure the multi-threading programming terminates gracefully. One of common practice is to join all children threads before returning from main() function.

Detach
std::thread::detach() is to tell this thread is to run in the background as daemon. After calling it std::thread object will become un-joinable and the parent thread will lose the control of the detached children thread because it does not hold a valid reference (std::thread object) to the children any more.

One of solutions to tackle this issue is to use RAII mechanism. Implement a class called like ScopedThread to call std::thread::join() or std::thread::detach() in the destructor if it is joinable.

3. Crashes
Thread Must Be Un-joinable before Out-Of-Life
C++11 standard says that std::terminate() will be called if a thread instance/object is still joinable before it comes out-of-life. This means that a std::thread object must call itself on function join() or detach() before its life comes out of scope, otherwise causes crash.

/*****************************************/
    struct Bar {
        void DoWork(int x, double y)
        {}
    };

    {
        Bar b;
        int x = 1;
        double y = 2.0;

        std::thread t1(&B:DoWork, b, x, y);
        // t1.join(); - block here until t1 returns
        // t1.deatch(); - t1 runs as a daemon
    } // must call join() or deatch() on t1 before hitting here.
  /*****************************************/

No Exception Thrown When Copying Arguments By Default
std::thread is to make a copy of every passing arguments into the function associated with it. C++11 standard says that std::terminated() is to be called if an exception is thrown during the copying process.

/*****************************************/
    struct Bar {
        Bar(const Bar&) {
            std::throw logic_error("Copy inhibited");
        }
        void DoWork(int x, double y)
        {}
    };

    Bar b;
    int x = 1;
    double y = 2.0;

    // crash as b is passed as an instance and a copy to be made
    // when creating std::thread object. But an exception is thrown
    // in Bar's copy constructor
    std::thread t1(&B:DoWork, b, x, y);
    // Fine reference/pointer is passed, Bar's copy-constructor not called
    std::thread t2(&B:DoWork, std::ref(b), x, y);
    std::thread t3(&B:DoWork, &b, x, y);
/*****************************************/

Dangling Reference or Pointer
This is nothing new for dangling reference/pointer causing crashes. But in std::thread there is one particular situation that need extra care - std::thread::deatch(), It means that a thread is running as daemon. It can still run on background when all other threads go out-of-scope. Make sure the daemon thread is not referring to any automatic variables from any other threads.

/*****************************************/
    struct Bar {
        void DoWork(int x, double& y)
        {
             // do some work and reference to y
        }
    };

    {
        Bar b;
        int x = 1;
        double y = 2.0;

        // object b is fine as passed as an object and therefore
        // an copy is made upon b.
        // but y is not ok. its reference is passed and cause dangling
        // if DoWork() has not return but t1.deatch() is called and run
        // out of this block.
        std::thread t1(&B:DoWork, b, x, std::ref(y));
        t1.deatch(); - t1 runs as a daemon
    }
  /*****************************************/

The Function Associated with std::thread Object Throw Exception but Uncaught
C++11 standard says that any exception cannot be propagated outside of std::thread object. Exceptions have to be caught inside std::thread object, otherwise std::terminated() is called. (C++11 provides other facilities to catch exception thrown outside thread like std::future, std::async, std::packaged_task and etc.)

/*****************************************/
    struct Bar {
        void DoWork(int x, double y)
        {
             std::throw logic_error("Causing crash");
        }
    };

    {
        Bar b;
        int x = 1;
        double y = 2.0;

        // std::terminated when DoWork() throws
        std::thread t1(&B:DoWork, b, x, y); 
        t1.join(); - block here until t1 returns
    }
  /*****************************************/

Ownership
std::thread is not copy-able but move-able. The left hand side has to point to an empty or become an un-joinable std::thread object. Otherwise the left hand side std::thread object is to be destroyed when it is still joinable before the right hand side object is moved into it. C++11 standard says this cause std::terminate() to be called.

/*****************************************/
    struct Bar {
        void DoWork(int x, double y)
        {
             std::throw logic_error("Causing crash");
        }
    };

    void Foo()
    {
        // do work
    }
    {
        std::thread t1(Foo);
        std::thread t2 = t1; // t1 points to nothing and t2 points to Foo
        t1 = std::thread(Foo); // t1 points to Foo too
        t2.deatch() // t2 becomes un-joinable now
        t2 = std::thread(Foo) // t2 points to Foo too
        t2 = t1; // Crash - t2 is still joinable
    }
  /*****************************************/

4. Deadlock
As I mentioned in Section 2 about std::thread::join(), it is a blocking function. Deadlock will happen if two threads are waiting for each other to terminate before they can carry on further.

/*****************************************/
    std::thread t1;
    std::thread t2
    void Bar()
    {
        // do some work
        t2.join(); // blocking until t2 to finish
        // continue work
    };

    void Foo()
    {
        // do some work
        t1.join(); // blocking until t1 to finish
        // continue work
    }

     // in parent thread
    {
        t1 = std::thread(Bar);
        t2 = std::thread(Foo);
    }
  /*****************************************/

It is quite similar with the deadlock happening to data-race, when there is cyclic-locking between multiple std::mutex. In this case there is also a cyclic joining between threads. The solution is also quite similar - avoid cyclic joining and always join in an order. Practically join all threads in the main thread.