Weekly C++ 7- std::thread (I)

It is time for another weekly C++ post. In this post, I am going to talk about a library which was first introduced in C++ 11. I have not mentioned that library in my Modern C++ post series (do not ask why), but it is time to write about it. I believe that most of you, at one point in your programs needed to use it. Well, it is thread library. As multicore CPUs become prevalent, developing multithreaded application become an important skill for every developer. For a long time, C++ developers, we are limited to platform dependent APIs (if not using boost like libraries) such as Win32 Sockets, MFC sockets, PThreads etc. To see how it can be handled before C++ 11 you can check some of references like this one.

Introduction:

With C++ 11, we are now able to develop multithreaded applications using STL’s Thread library in a standard way for multiple platforms. If you have been using Boost Thread library then you will be very happy to know that C++ 11 thread library mostly based on Boost. However, I believe that, if you do not have any other reason to stick with Boost or other libraries, you should always use standard C++ thread library std::thread.

I am planning to start with a very brief introduction about threads, then describe core capabilities with sample codes and explain the rest of thread topics in another post. So if you could not find a subject in this post, then it will be in next post or you can directly ask it via comment section.

A very gentle introduction to threads:

Well, as all you know I usually do not delve into details in our weekly C++ posts 🙂 but let us have a brief look at what threads are? To check out more detailed resources you may read Processes and Threads chapter of Tanenbaum’s Modern Operating Systems book or Anthony Williams C++ Concurreny in Action books. In this section, I will give you summary of topics mostly covered in sites that are provided in references so for details or unclear points you may check there.
To understand threads, we need to look at programs, processes, threads and their relationship. The programs are the binary files that are obtained through compilation of your source code (in case of C++ of course :), if it is written in Python or JavaScript it is interpreted at runtime) which are usually reside on disks till they are loaded into memory for execution.
After this program is loaded into memory it become process with all associated resources. These processes are managed by operating system and may have additional parameters which may differ from one to another, but all of them have following information:

  • Program/Instruction counter: the current position in the program (i.e. which instruction is currently being executed),
  • Registers: Data slots that resides at CPU
  • Stack: Data structure that stores information about the active subroutines of a computer program and is used as scratch space for the process
  • Heap: Dynamically allocated memory
  • File/Socket handles/Signals: The files and sockets that are opened by this processSignals, etc.

Each of these processes have separate memory address spaces and they are executed independently and isolated from other processes. These cannot directly access shared data in other processes. This independency is important because the OS tries to isolate processes so that a problem with one process doesn’t affect or corrupt other processes.

Now it is time to look at threads. Thread is the unit of execution within process, set of instructions within program/process that can be executed independently from other code. A process may have one or more threads. In some resources, threads are called lightweight processes because they have their own stack (not shared with other threads) but they can access processor’s shared data such as heap memory, sockets and files. The threads reside at same process share the same address space, so the operational cost of communication between the threads is low with respect to inter-process communication, which is an advantage. However, there is still a possiblity that a bug/problem within one thread in a process will certainly affect other threads and process itself.

The relationship between a process and thread is provided in below figure from this reference:

Now, let us have a look at the differences between processes and threads (Computer Engineer students, this list may help you for your OS midterms 🙂

  • Processes are usually used for heavyweight operations, threads for lighter ones,
  • Processes do not share memory with other processes. Threads share memory with other threads of owning process,
  • Inter-thread communication is usually faster than inter-processone because threads share same memory,
  • Context switching between threads of the same process is less expensive than context switching processes.

The choice between process or thread is not our concern but above differences may give you an idea. Besides, there is an example use case about this decision from google.

Now, let us look at another two concepts that are related with this topic which are parallelism and concurrency. If the computer that you are using has only one core, the threads you created will run in parallel but never concurrently, because a core can only execute one thread instruction at the same time. In other words, every thread is executed for some fraction of time of core, then paused and other thread resumed and uses its time which can be viewed as an illusion of concurrency. If there are multiple core and each thread runs in seperate core, then we achieve a true concurreny.

It should be noted that you have no control over the order of thread time share on CPU, their priority which is under control of OS and no standard (via STL) way is provided. Of course, these threads can be scheduled manually by using other constructs such as mutexes or condition variables.

Finally, before closing this section, there are two import points that you should take into consideration before using multihreading:

  • First one is to understand the problem that will be solved using multithreading, which parts of problem can be parallel and if problem is suitable for that. There is a well known phrase that based on Pareto principle which says that “90% of processor cycles are spent in 10% of the code.”. So do not try to use multithreading for 90% part 🙂
  • Second one is shared resources. How can these threads share data safely and do not waste time for waiting resources or worse cause deadlocks. This is where thread synchronization primitives are used.

Thread basics:

Now let us return to usage of std::thread library. We will start with thread basics and how to obtain information about threads that we created. The compiler requirements for using threads are provided below:

Windows: Visual Studio 2012

Linux: gcc 4.8.1

To use thread library, <thread> header should be included. The simplest way to initiate a thread in addition to main thread (every C++ application start with a default thread) is to create a std::thread instance with a callable item which can be:

  • A stand alone function or class static member,
  • Class member function pointer in which case the corresponding class object should also be passed,
  • Function objects,
  • Lambda function.

The corresponding thread will start just after creation. There is also a namespace which is called std::this_thread which provides some supporting functions that can be used for current thread. In fact, there are four of them which are:

  • sleep_for(): Put current thread into sleep for provided time.
  • get_id(): Get an unique identification for current thread.
  • yield(): The calling thread yields, offering the implementation the opportunity to reschedule.
  • sleep_until(): Put current thread into sleep/blocks until provided time when the calling thread shall resume its execution.

In following example, we are creating four additional thread to main one with each of above callbacks. Additionally, we provided some example API usages for mentioned std::this_thread:

The number of threads that can be executed concurrently can be determined using std::thread::hardware_ concurrency() as illustrated below. This information can be useful especially, if you are planning to distribute task with respect to platform processor power.

Joining/detaching threads:

Now we have covered different ways of creating threads, now it is time to check out the correct way to complete threads which can be either by joining or detaching.

When any of above thread callback function is completed, library performs some thread completion tasks and OS level stuff.

If we look at the sample code given above, if we return without calling any join() API, then the whole application is terminated and also threads without waiting for threads which may cause unexpected behavior. So you should always call either join or detach. The join() API put caller thread to sleep until corresponding callee thread callback returns. Of course, it is important to know that this callback function will eventually return, otherwise you wait forever 🙂 If you do not want your caller thread sleep for given callee thread then you should call detach() API. From that point, we have no control over that thread any more.

It is also important to not call join or detach() APIs on threads that have no association with current thread (i.e. already joined or detached or no association at all) which may cause program to terminate.

Passing parameters to threads:

It is time to look at how we can pass parameters to threads. Well, we had already shown one example in above sample code for class member callback example where you just pass argument to thread constructor. So it is as simple as passing the parameters to thread function, but you should consider variable scope and its validity. For instance, if you pass a heap variable, you should ensure that caller should not deallocate it while callee thread using that variable or local variable get out of scope like shown below:

To prevent that a new std::string object could be created and passed to thread.

There is one more case which should be taken into consideration where you want to pass a reference to thread function. Let us look the sample code below:

Even if the execute function is defined as expecting reference as parameter, the constructor of the new thread just copies internally str variable. When the thread calls the execute function, it sends as parameter a reference to the internal copy of the str variable. Therefore, when the new thread completes its execution, str variable (with the new value “Updated Hello World!”) is 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 str variable remains unchanged. The reason for this is the parameter mechanism of thread where all parameters passed to thread are copied into internal storage of new thread by default.

To overcome this problem, the parameter itself can be send as reference through using std::ref() function which is illustrated below:

Passing thread possesion:

One last think I would like to write here is about thread ownership. I like the analogy given in this post which briefly say that thread is similar to std::unique_ptr which need to be moved not copied. So whenever you created a thread object, you could not just copy that object (you may try :). You need to use std::move() (check out my previous posts about this API). So when you may need to move possesion. Well, as other posts mentioned, you may want to design a class which creates a thread that will runs in the background or you may also just want to transfer the ownership of thread to another function. In case of a new thread start and association with a temporary thread object, the transfer of ownership doesn’t require a explicit call to std::move() for moving ownership, because the owner is a temporary object and moving from temporaries is automatic and implicit.

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:

Let me close this section with another sample code that shows the possible use of std::vector with threads which might be useful for thread pool like structs.

Next topics:

I am planning to write about thread synchronization constructs, atomics and async in my next post. Take care of yourself and start using these stuff 🙂

References:

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.