We will start multiple threads and see how our program behaves when we unleash multiple processor cores to execute parts of its code at the same time:
- At first, we need to include only two headers and then we declare that we use the std and chrono_literals namespaces:
#include <iostream>
#include <thread>
using namespace std;
using namespace chrono_literals;
- In order to start a thread, we need to be able to tell what code should be executed by it. So, let's define a function that can be executed. Functions are natural potential entry points for threads. The example function accepts an argument, i, which acts as the thread ID. This way we can tell which print line came from which thread later. Additionally, we use the thread ID to let all threads wait for different amounts of time, so we can be sure that they do not try to use cout at exactly the same time. If they did, that would garble the output. Another recipe in this chapter deals specifically with this problem:
static void thread_with_param(int i)
{
this_thread::sleep_for(1ms * i);
cout << "Hello from thread " << i << 'n';
this_thread::sleep_for(1s * i);
cout << "Bye from thread " << i << 'n';
}
- In the main function, we can, just out of curiosity, print how many threads can be run at the same time, using std::thread::hardware_concurrency. This depends on how many cores the machine really has and how many cores are supported by the STL implementation. This means that this might be a different number on every other computer:
int main()
{
cout << thread::hardware_concurrency()
<< " concurrent threads are supported.n";
- Let's now finally start threads. With different IDs for each one, we start three threads. When instantiating a thread with an expression such as thread t {f, x}, this leads to a call of f(x) by the new thread. This ,way we can give the thread_with_param functions different arguments for each thread:
thread t1 {thread_with_param, 1};
thread t2 {thread_with_param, 2};
thread t3 {thread_with_param, 3};
- Since these threads are freely running, we need to stop them again when they are done with their work. We do this using the join function. It will block the calling thread until the thread we try to join returns:
t1.join();
t2.join();
- An alternative to joining is detaching. If we do not call join or detach, the whole application will be terminated with a lot of smoke and noise as soon as the destructor of the thread object is executed. By calling detach, we tell thread that we really want to let thread number 3 to continue running, even after its thread instance is destructed:
t3.detach();
- Before quitting the main function and the whole program, we print another message:
cout << "Threads joined.n";
}
- Compiling and running the code shows the following output. We can see that my machine has eight CPU cores. Then, we see the hello messages from all the threads, but the bye messages only from the two threads we actually joined. Thread 3 is still in its waiting period of 3 seconds, but the whole program does already terminate after the second thread has finished waiting for 2 seconds. This way, we cannot see the bye message from thread 3 because it was simply killed without any chance for completion (and without noise):
$ ./threads
8 concurrent threads are supported.
Hello from thread 1
Hello from thread 2
Hello from thread 3
Bye from thread 1
Bye from thread 2
Threads joined.