[C++] Multithreading and Concurrency: INTRODUCTION
By: Valentina Di Vincenzo
Tags: Cpp, Programming, Multithreading, Synchronization, Concurrency

Today, let’s quickly refresh some basic concepts and then taste a bit of concurrent code.

1. What is a thread?

Upon creation, each process has a unique thread of execution called main thread. It can ask the Operating System to create other threads, that share the same address space of the parent process (code section, data section, other operating-system resources, such as open files and signals). On the other hand, each thread has its own thread ID, stack, register set, and program counter. Basically a thread is a lightweight process, switching between threads is faster and IPC is easier.

2. What is concurrency?

The scheduler allocates, over time, the use of available cores between different threads in a non-deterministic way. This is called hardware concurrency: multiple threads running on different cores in parallel, each of them taking care of a specific task of the program.

→ N.B. The std::thread::hardware_concurrency() function is used to know how many tasks the hardware can truly run concurrently. If the number of threads exceeds this limit, we will possibly incur excessive task switching (switching between tasks many times per second to give an illusion of concurrency).

3. Basic thread operations with std::thread

Header | #include <thread>

| Launching a thread | std::thread t(callable_object, arg1, arg2, ..)

This creates a new thread of execution associated with t, which calls callable_object(arg1, arg2) . The callable object (i.e. a function pointer, a lambda expression, the instance of a class with a function call operator ) is immediately invoked by the new thread, with the (optionally) passed arguments. By default they are copied, if you want to pass by reference you have to warp the argument using std::ref(arg) . Also, remember that if you want to pass a unique_ptr you must move it ( std::move(my_pointer) ), since it cannot be copied.

| This creates a new thread of execution associated with t, which calls . The callable object (i.e. a function pointer, a lambda expression, the instance of a class with a function call ) is immediately invoked by the new thread, with the (optionally) passed arguments. By default they are copied, if you want to pass by reference you have to warp the argument using . Also, remember that if you want to pass a unique_ptr you must move it ( ), since it cannot be copied. Thread life-cycle | t. join () and t. detach ()

If the main thread exits, all the secondary threads still running suddenly terminate, without any possibility of recovery. To prevent this to happen, the parent thread has two options for each child:

→ Blocks and waits for the child termination, by invoking the join method on the child.

→ Explicitly declaring that the child can continue its execution even after the parent’s exit, using the detach method.

| and If the main thread exits, all the secondary threads still running suddenly terminate, without any possibility of recovery. To prevent this to happen, the parent thread has two options for each child: → Blocks and waits for the child termination, by invoking the join method on the child. → Explicitly declaring that the child can continue its execution even after the parent’s exit, using the detach method. To remember: a thread object is just movable, not copyable.

Here you can find a small code example of almost all the above theory.

4. Why is synchronization needed?

With multiple threads sharing the same address space and resources, a lot of operations become critic: multithreading requires synchronization primitives. This is why.

Memory is a haunted house

Memory cannot be considered as a “static warehouse” anymore, unless not haunted. Imagine: a thread is comfortably watching Netflix on a Smart TV, it blinks and the TV is gone. Panicking, it dials 911 on the phone and…“Thank you for calling Pizza Hut”. What is going on? Simply, the house is full ghosts (where ghosts are other threads): they are all in the same room, interacting with the same objects (this is called a data race), but they are ghosts for one another.

In order to act safely, a thread should declare what is using and check if the object is in use prior to touch it. Is thread Green watching TV? Well, then no one can touch the TV in any way (if anything, others can sit and watch it too). This can be done with a mutex.