A thread represents a single flow of execution within a program. Each program will have at least one thread of execution that will start executing main. The statements within that thread will execute according to the normal C++ sequencing rules.
The creation of a second thread provides another means of execution. Each thread has an individual flow of statements, and the two threads can proceed as quickly as they want to at the same time, and can even execute simultaneously if the system has the appropriate hardware and the operating system puts them on different processor cores.
C++ standard class for creating a second thread is std::thread. The easiest method is to pass in the name of a normal C++ function (or some other callable object) that takes no arguments.
#include <iostream>
#include <thread>
void printMessage()
{
std::cout << "hello from the thread\n";
}
int main()
{
std::thread t{ printMessage };
t.join();
std::cout << "hello from main\n";
return 0;
}
When writing std::thread t{ printMessage };, you are creating object t, and starting a thread that will call printMessage. It is unknown when exactly the new thread will be granted execution time by the operating system.
The object t is a handle for the newly created thread. t can be used for various thread related operations such as join/detach the thread or get the thread ID.
If you call the join() method of a thread object, the thread that makes this call will wait until the thread represented by the object has died. In the above example, the thread that runs main will be blocked on t.join() until printMessage finishes. Whatever statements are after the call to join() cannot be executed until this call returns. We will learn about joining and deatching threads in the next lesson.
After printMessage terminates, join() returns so the main thread can continue. It then prints the second message.
The output therefore always appears in this order:
hello from the thread
hello from main
However, it is problematic to destroy a joinable std::thread object before it has been joined or detached, its destructor will call std::terminate(), which will immediately terminate the program.
#include <iostream>
#include <thread>
void printMessage()
{
std::cout << "hello from the thread\n";
}
int main()
{
std::thread t{ printMessage };
std::cout << "hello from main\n";
return 0;
}
t is destroyed as execution leaves main. This thread was still joinable because it had not been joined nor detached. Hence the call to std::terminate() in the destructor of t.
In fact, depending on thread scheduling and output buffering, one of the messages may be printed before the program is terminated, but you cannot be sure that either message will be seen, nor can you be sure which thread will get access to the CPU first.
A joinable std::thread has to be joined or detached before the thread object is destroyed. In this case, calling join() is the best solution as we need to wait for the new thread to complete before exiting main.
Alternatively, you can also create a thread which runs a function with arguments. The arguments are written after the function name in the constructor of the class std::thread.
#include <iostream>
#include <thread>
void printSum(int a, int b)
{
std::cout << a + b << '\n';
}
int main()
{
std::thread t{ printSum, 3, 4 };
t.join();
return 0;
}
A fresh thread gets created here, which invokes printSum with two parameters 3 and 4 using the statement std::thread t{ printSum, 3, 4 };.
When I call join(), the thread running main does not continue until the printSum thread has completed, so I get:
7
When you create std::thread, the arguments will be copied or moved in internal storage by default. And the new thread uses those stored values when it calls the function.
Values copied/moved by themselves do not need the original object to stay alive past the moment the std::thread constructor ends. Copying a pointer or other non owning reference doesn't keep the object alive though; any object indirectly touched by the new thread must stay alive as long as the thread potentially may use it.
If you want to pass an argument by reference to the thread function, you have to wrap the reference by means of a reference wrapper (e.g. std::ref). You then have to make sure that the object being referenced stays alive until the thread can possibly use it.
The thread object can be joined only once. At the first successful invocation of join(), the object becomes non joinable and subsequent calls to join() throw std::system_error.
You can use the member function joinable() to determine whether the object currently represents a joinable thread.
if (t.joinable())
{
t.join();
}
In most simple code, the easiest thing to do is to call join() only once when you want to wait for the thread to complete.
Since the new thread runs in parallel to the main thread, the order of statements from the main thread and the new thread is not consistent every time the program runs. However, statements within each thread still follow the normal rules of C++.
A successful call to join() provides an ordering guarantee. The thread that calls join() will not return until the thread joined to has completed executing. Therefore, all actions performed by the joined thread must occur before all actions by the thread calling join() after join() has returned.
But the synchronization provided by join() does not guarantee the interleaving of thread execution. For more fine grained synchronization of thread execution, other synchronization means like mutexes, condition variables, atomic operations, etc. will be needed.
Creating a std::thread begins a new thread of execution and returns an object that represents it. This object must be joined or detached before it is destroyed. Calling join() will block the thread that calls it until the thread it is called on completes.
0 comments
No comments yet. Be the first.