EDITING BOARD
RO
EN
×
▼ BROWSE ISSUES ▼
Issue 19

Multithreading in C++11 standard (part 1)

Dumitrița Munteanu
Software engineer
@Arobs
PROGRAMMING

Thirtheen years after publishing the first C++ standard, and simultaneous with the publication of the new C++11 (or C++0x) standard, the members of the C++ Standards Committee decided to offer a major change regarding the multithread programming. For the first time C++ language offers support in implementing applications that require concurrent programming, regardless of the development platform. Before C++11 standard the multithreaded applications were based on platform-specific extensions, like Intel TBB, OpenMP, Pthreads, etc.

Portable applications are the major advantage brought by this new feature (i.e. Windows multithreaded applications are easily proted to iPhone or Android platforms). An advantage for those familiariazed with Boost thread library is that many concepts from the standard C++11 library keep the same name and structure as the boost threads library classes.

The C++ Standard Library contains classes for thread manipulation and synchronization, common protected data and low-level atomic operations. Next we will exemplify and generally describe how these concepts occur in C++11 Standard.

Starting a thread

Starting a thread in C++11 is as simple as declaring and instantiating a new object. We will analyze a simple multithreaded application in order to demonstrate how we can use the threads from the C++ standard library. For example:


#include 
#include  //-- (1)
void execute()
{
    std::cout << "Hello Concurent World" << std::endl;
}
int main()
{ 
    std::thread worker_thread(execute); //-- (2)

worker_thread ().join(); //--(3)
}

The first difference is the inclusion of the header file #include (1). The header file contains functions and classes for thread management. Threads are started by defining an object std::thread, that specifies in it"s constructor an initial method that will be execute by the thread, in our case the execute() method, which is the place where the new thread will start it"s execution. After the thread was launched and before the std::thread object is destroyed by exiting the main function, it must be explicitly specified if the main thread is supposed to wait until the secondary thread completes execution (by calling the join() (3)) function or if the secondary thread will run independently of the life cycle of the main thread (by calling the detached() method). In this case the thread will run in the background, with no mean to comunicate with it. Detached threads, also known as daemon threads, are named after a Unix OS concept (when a daemon process runs in the background, without a specific interface).

Passing parameters to a thread function

Passing parameters to a new thread in C++11 is as simple as passing the parameters to a callable object. It is important to note that the parameters are passed as local copies in the thread"s stack, whence they can be accessed by the running thread, even if the corresponding parameters of the function expect reference. For example:

void execute(const std::string& filename);
std::thread worker_thread(execute, "input.dat");

In this case a new thread is created, associated with worker_thread variable, which calls the execute("input.dat") function. Pay attention to the the passed parameter. Even if the execute function receives a reference of std::string as a parameter, the string is passed as const char* and converted only in the new thread to std::string. The manner in which passing parameters to a std::thread function works, can lead to two possible problems.

The first case would be the one in which the transmitted parameter is a pointer to a local variable. For example:

void execute(const std::string& filename);
void oops(const char* parameter)
{
    char buffer[50];
    sprintf(buffer, "%s", parameter);
    std::thread worker_thread(execute, buffer); //-- (1) 
    worker_thread.detach();
}

In this example, a pointer to the local variable buffer is passed to the new thread, that waits for a std::string type parameter. It is possible that the oops function completes before its conversion from char* to std::string takes place. The passed pointer becomes a dangling pointer (١) and the thread behaviour will be undefied.

In this situation the solution would be the explicit conversion of the buffer variable to a std::string, before the parameter is being passed to the constructor of the new thread:

std::thread worker_thread(do_work, std::string(buffer));


The second case would be where the passed parameter is copied, even if the intention was to send a reference of the object whose value had to be changed by the thread. For example:

void execute(std::string& str) //-- (1) 
{
    str.assign("Hallo Welt!"); 
}
void oops(const char* parameter)
{ 
    std::string greeting("Hello World!");
    std::thread worker_thread(execute, greeting); //-- (2) 
    worker_thread.join();
    std::cout<< greeting << std::endl; //-- (3)
}

Even if the execute function (1) is waiting for a reference as parameter, the constructor of the new thread doesn"t know this, which is why it just copies internally my_string variable. When the thread calls the execute function (2), it will send as parameter a reference to the internal copy of the greeting variable. Therefore, when the new thread completes its execution, my_string variable (with the new value "Hallo Welt") will be destroyed together with the destruction of the internal copies of the parameters passed to the constructor of the thread. For this reason, the value of the greeting variable remains unchanged, showing the initial value "Hello World" (3).

In this case the solution would be to transmit the parameters that must indeed be a reference, using the std :: ref function.

std::thread worker_thread(execute, std::ref(greeting));

The std::ref function is available, too, only with C++١١ standard and it"s a method used to simulate the reference feature so that, in the end, my_string variable (٣) will have the changed value "Hallo Welt!".

For those familiar with the std::bind function, the semantics of passing a pointer to a member function of an object as parameter to the constructor of a std::thread may be surprising. For example:

class Test
{
    public: 
    void execute();
};

Test custom_test;
std::thread worker_thread(&Test::execute, &custom_test); //-- (1)


This code will launch a thread which will run the custom_test.execute() method. If the execute() method recievs parameters, they will be transmitted in the same way, as the third, fourth, etc. for the constructor of the current thread.

Transfering thread possession

Suppose you want a std::thread create_thread() function to create a thread that runs in the background, but which, instead of waiting for the new thread to complete it"s execution, returns the new thread to the function from which it was called. Or we can assume that the function creates a thread and send it"s ownership to some function that has to wait for the newly created thread to complete its execution.

In both cases it is necessary to transfer the possession of std::thread which, similar to a std::ifstream and a std::unique_ptr can be transferred, but not copied. For example:

void some_function(int n);
std::thread create_thread()
{
std::thread my_thread(some_function, 24);
return my_thread; 
} 

std::thread first_thread (some_function, 25); //-- (1)
std::thread second_thread = std::move(t1); //-- (2)
first_thread = create_thread(); //-- (3)


Firstly, a new thread is launched (1) and it is associated with first_thread variable. The possession of the new thread is then thransfered towards the second_thread variable (2). The next possesion transfer (3) doesn"t require calling the std::move function because the current owner is a temporary object and the transfer is automatic and implicit.

Synchronization mechanisms

For synchronization between threads, C + +11 standard provides classical synchronization mechanisms like mutex objects (std :: mutex, std :: recursive_mutex, etc.), condition variables (std::condition_variable, std::condition_variable_any), which can be accessed through RAII locks (resource acquisition is initialization, std::lock_quard andstd::unique_lock) and other mechanisms called futures and promises used to transfer results between threads or std::package_task in order to "pack" a call to a function that can generate such a result.

Mutex

In the previous examples we have seen how parameters can be launched and passed to a thread function. This mechanism, however, is not enough when one wants certain resources to be used (modified) by multiple threads running simultaneously.

In this situation it is necessary to use a mutual exclusion mechanism that ensures data integrity when there is the possibility for multiple threads to modify the same resource at the same time.

The most commonly used synchronization primitive is the mutex. Prior to access resources that can be simultaneously modified by different threads, a thread must lock the mutex associated with the shared resource and when the thread is no longer operating on shared data, it must be unlocked. If a mutex is already locked and another thread tries to lock it again, it must wait until the thread that has successfully locked the mutex unlocks it.

The C+١١ standard provides std::mutex primitive, which can be used by including the header #include. A std::mutex object also provides member functions - lock() andunlock( ) - to explicitly lock or unlock a mutex. The most common use of a mutex is when one wants to protect a particular block of code.

To this end the C++ standard library provides the std::lock_guard< > template, whose mechanism is based on the RAII principle (resource aquisition is initialization). The mutex is blocked in the std::lock_quard object"s constructor and automatically unlocked in obeject"s destructor. For example:

std::mutex my_mutex;
unsigned int counter = 0;
unsigned int increment()
{
std::lock_quard lock_counter(my_mutex);
return ++counter;
}
unsigned int query()
{
std::lock_quard lock_counter(m);
return counter;
}

In this example, the access to the counter variable is serialized. If more than one thread concurrently calls the query() method, then they will be blocked until the thread that has successfully locked the mutex will unlock it. Since both functions lock the same mutex, if a thread calls query() method and another calls the increment( ) at the same time, then only one of them will be able to lock the mutex and to access the counter variable.

When questioning exception handling, a std::lock_quard< > type variable brings additional benefits compared to manual unlock by directly calling the methods lock() and unlock( ) on a mutex. When using the manual lock, one must make sure that the mutex is unlocked at each exit from the protected region, including the regions in which it prematurely terminates execution, due to the launch of an exception. By using a std::lock_quard object, this safe behaviour is assured, because by launching an exception, the destructor of the std::lock_quard object will be automatically called due to the stack unwind mechanism.




Sponsors

  • comply advantage
  • ntt data
  • 3PillarGlobal
  • Betfair
  • Telenav
  • Accenture
  • Siemens
  • Bosch
  • FlowTraders
  • MHP
  • Connatix
  • UIPatj
  • MetroSystems
  • Globant
  • Colors in projects