Joining exists because threads need to communicate with one another. After the change has taken place, the called thread may change the value of a global variable, which the calling thread needs to access. This is a form of synchronization.
This article explains two ways of joining threads. It begins with an illustration of what a thread is.
Article Content
Thread
Consider the following program:
using namespace std;
void fn2() {
cout << "function 2" << '\n';
}
void fn1() {
cout << "function 1" << '\n';
}
int main()
{
/* some statements */
return 0;
}
fn1(), fn2() and main() are top-level functions, though main() is a key function. Three threads can be obtained from these three top-level functions. The following very simple short program, is a natural thread:
using namespace std;
int main()
{
/* some statements */
return 0;
}
The main() function behaves like a thread. It can be considered as the main thread. It does not need to be encapsulated in any instantiation from the thread class. So, the previous program with top-level functions that includes the main() function is still one thread. The following program shows how the two functions, fn1() and fn2() can be converted into threads, but without any join statement:
#include <thread>
#include <string>
using namespace std;
string globl1 = string("So, ");
string globl2 = string("be it!");
void fn2(string st2) {
string globl = globl1 + st2;
cout << globl << endl;
}
void fn1(string st1) {
globl1 = "Yes. " + st1;
thread thr2(fn2, globl2);
}
int main()
{
thread thr1(fn1, globl1);
/* some statements */
return 0;
}
The program begins with the inclusion of the iostream library for the cout object. Then the thread library is included. Including the thread library is a must; so that the programmer will simply just instantiate a thread object from the thread class using a top-level function, except for the main() function.
After that, the string library is included. This library simplifies the use of string literals. The first statement in the program insists that any name used is from the C++ standard namespace unless otherwise indicated.
The next two statements declare two global string objects with their literals. The string objects are called globl1 and globl2. There is the fn1() function and the fn2() function. The fn2() function's name will be used as one of the arguments for instantiating the thread, thr2, from the thread class. When a function is instantiated in this way, the function is called; and it executes. When the fn2() function is called, it concatenates the string literals of globl1 and globl2 to have “So, be it!”. globl2 is the argument of fn2().
The name of the fn1() function is used as an argument to the instantiation of the thread, thr1, from the thread class. During this instantiation, fn1() is called. When it is called, it precedes the string, “So, be it!” with "Yes. ", to have "Yes. So, be it!" , which is the output for the whole program of threads.
The main() function, which is the main() thread, instantiates the thread, thr1 from the thread class, with the arguments fn1 and globl1. During this instantiation, fn1() is called. The function, fn1() is the effective thread for the object, thr1. When a function is called, it should run from the beginning to its end.
thr1, which is effectively fn1(), instantiates the thread, thr2, from the thread class, with the arguments fn2 and globl2. During this instantiation, fn2() is called. The function, fn2() is the effective thread for the object, thr2. When a function is called, it should run from the beginning to its end.
If the reader is using, the g++ compiler, then he can test this program of threads, for C++20 compilation, with the following command:
The author did this; ran the program and had the output:
Aborted (core dumped)
It is possible to have an erroneous output like this, with the proper output of "Yes. So, be it!", slotted within. However, all that is still unacceptable.
The problem with this erroneous output is that threads were not joined. And so, the threads operated independently, leading to confusion. The solution is to join thr1 to the main thread, and since thr1 calls thr2, just as the main thread calls thr1, thr2 has to be joined to thr1. This is illustrated in the next section.
Joining a Thread
The syntax to join a thread to the calling thread is:
where join() is a member function of a thread object. This expression has to be inside the body of the calling thread. This expression has to be inside the body of the calling function, which is an effective thread.
The following program is the above program repeated, but with the body of the main thread joining thr1, and the body of thr1 joining thr2:
#include <thread>
#include <string>
using namespace std;
string globl1 = string("So, ");
string globl2 = string("be it!");
void fn2(string st2) {
string globl = globl1 + st2;
cout << globl << endl;
}
void fn1(string st1) {
globl1 = "Yes. " + st1;
thread thr2(fn2, globl2);
thr2.join();
}
int main()
{
thread thr1(fn1, globl1);
thr1.join();
/* some statements */
return 0;
}
Note the waiting positions, where the join statements have been inserted into the program. The output is:
without the quotes, as expected, clean and clear, unambiguous”. thr2 does not need any join statement in its body; it does not call any thread.
So, the body of the calling thread joins the called thread.
future::get()
The C++ standard library has a sub-library called future. This sub-library has a class called future. The library also has a function called async(). The class, future, has a member function called get(). In addition to its main role, this function has the effect of joining two functions that are running concurrently or in parallel. The functions do not have to be threads.
The async() Function
Notice that the threads above all return void. A thread is a function that is under control. Some functions do not return void but return something. So, some threads return something.
The async() function can take a top-level function as an argument and run the function concurrently or in parallel with the calling function. In this case, there are no threads, just a calling function and a called function called an argument to the async() function. A simple syntax for the async function is:
The async function returns a future object. The first argument here, for the async function, is the name of the top-level function. There can be more than one argument after this. The rest of the arguments are arguments to the top-level function.
If the top-level function returns a value, then that value will be a member of the future object. And this is one way to mimic a thread that returns a value.
future::get()
The async function returns a future object. This future object has the return value of the function that is an argument to the async function. In order to obtain this value, the get() member function of the future object has to be used. The scenario is:
Type futObj.get();
When the get() function is called, the body of the calling function waits (blocks), until the async function has returned its value. After that, the rest of the statements below the get() statement continue to execute.
The following program is the above one, where no thread has been created officially, and in place of the join statement, the get() statement has been used. The async() function has been used to simulate a thread. The two top-level functions have been reduced to one. The program is:
#include <future>
#include <string>
using namespace std;
string globl1 = string("So, ");
string globl2 = string("be it!");
string fn(string st1, string st2) {
string concat = st1 + st2;
return concat;
}
int main()
{
future fut = async(fn, globl1, globl2);
string ret = fut.get(); // main() waits here
string result = "Yes. " + ret;
cout << result << endl;
return 0;
}
Note that the future library, instead of the thread library, has been included. The output is:
Conclusion
When one join statement is concerned, two top-level functions are involved. One is the calling function, and the other is the called function. In the body of the calling function is the join statement. These two functions can be each encapsulated in a thread. The join() member function of the called thread is in the body of the calling thread. When the join() function is called, the calling thread waits at that point (blocks) until the called thread completes; before it continues to operate.
The use of threads can be avoided by using the async() function in the future library. This function takes a top-level function as an argument and returns a future object, which contains the returned value of the argument function to the async() function. In order to obtain the return value of the function as an argument, the get() member function of the future object has to be used. When the get() member function is called, the calling function body waits at that point (blocks) until the called function completes; before it continues to operate.