线程就像一个程序的子程序。因此,它们会独立运行,除非它们被加入。连接意味着一个线程将运行到某一点,然后停止并等待另一个线程完成其执行(到其结束);然后再继续自己的执行。在第一个线程停止的那一点上,有一个连接语句。一个线程可以调用另一个线程。调用的线程才是加入的。被调用的线程并没有加入。
加入之所以存在,是因为线程需要相互通信。在变化发生后,被调用的线程可能会改变一个全局变量的值,而调用的线程需要访问该变量。这是一种同步的形式。
本文解释了连接线程的两种方法。它以一个关于什么是线程的插图开始。
文章内容
线程
考虑一下下面的程序。
#include <iostream>
using namespace std;
void fn2() {
cout << "function 2" << '\n';
}
void fn1() {
cout << "function 1" << '\n';
}
int main()
{
/* some statements */
return 0;
}
fn1()、fn2()和main()是顶级函数,不过main()是一个关键函数。从这三个顶层函数可以得到三个线程。下面这个非常简单的短程序,就是一个自然的线程。
#include <iostream>
using namespace std;
int main()
{
/* some statements */
return 0;
}
main()函数的行为像一个线程。它可以被看作是主线程。它不需要被封装在任何来自线程类的实例中。所以,前面的程序中的顶层函数包括main()函数,仍然是一个线程。下面的程序显示了如何将两个函数fn1()和fn2()转换为线程,但没有任何连接语句。
#include <iostream>
#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;
}
程序开始时,包含了cout对象的iostream库。然后是包含线程库。包含线程库是必须的;这样,除了main()函数外,程序员将简单地使用顶层函数从线程类中实例化一个线程对象。
此后,字符串库也被包含在内。这个库简化了对字符串字面的使用。程序中的第一条语句坚持认为,除非另有说明,否则所使用的任何名称都来自C++标准名称空间。
接下来的两条语句声明了两个全局字符串对象和它们的字面意义。这些字符串对象被称为globl1和globl2。有fn1()函数和fn2()函数。fn2()函数的名称将被用作从线程类中实例化线程 thr2 的参数之一。当一个函数以这种方式被实例化时,该函数被调用;并且它被执行。当fn2()函数被调用时,它将globl1和globl2的字符串字面连接起来,得到 "So, be it!"。globl2是fn2()的参数。
fn1()函数的名称被用作线程类的实例化参数,即thr1。在这个实例化过程中,fn1()被调用。当它被调用时,它在字符串 "So, be it!"前加上 "Yes. ",以得到 "Yes. So, be it!" ,这是整个线程程序的输出。
main()函数,也就是main()线程,从线程类中实例化了线程thr1,参数为fn1和globl1。在这个实例化过程中,fn1()被调用。函数fn1()是对象thr1的有效线程。当一个函数被调用时,它应该从头运行到尾。
thr1,也就是有效的fn1(),从线程类中实例化了线程thr2,参数为fn2和globl2。在这个实例化过程中,fn2()被调用。函数fn2()是对象three2的有效线程。当一个函数被调用时,它应该从头运行到尾。
如果读者使用的是g++编译器,那么他可以用以下命令测试这个线程程序,用于C++20的编译。
g++ -std=c++2a temp.cpp -lpthread -o temp
作者这样做了;运行该程序,并有输出。
terminate called without an active exception
Aborted (core dumped)
像这样一个错误的输出是可能的,而正确的输出是 "是的,就这样吧!",这句话被夹在里面了。然而,所有这些仍然是不可接受的。
这个错误的输出的问题是,线程没有被加入。于是,这些线程独立运行,导致了混乱。解决办法是将thr1加入主线程,由于thr1调用thr2,就像主线程调用thr1一样,thr2必须加入thr1。这将在下一节中说明。
连接线程
将一个线程加入到调用线程中的语法是。
threadObj.join();
其中join()是线程对象的一个成员函数。这个表达式必须在调用线程的主体内。这个表达式必须在调用函数的主体内,它是一个有效的线程。
下面的程序是上述程序的重复,但主线程的主体加入了thr1,而thr1的主体加入了thr2。
#include <iostream>
#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;
}
注意等待的位置,连接语句已经被插入到程序中。输出结果是。
“Yes. So, be it!”
没有引号,正如预期的那样,干净利落,毫不含糊"。 thr2在其主体中不需要任何连接语句;它不调用任何线程。
所以,调用线程的主体加入了被调用的线程。
future::get()
C++标准库有一个名为future的子库。这个子库有一个名为future的类。这个库也有一个叫做async()的函数。future这个类有一个叫get()的成员函数。除了它的主要作用外,这个函数还有连接两个同时或并行运行的函数的作用。这些函数不一定是线程。
async()函数
请注意,上面的线程都返回void。一个线程是一个受控制的函数。有些函数并不返回void,而是返回一些东西。所以,有些线程会返回一些东西。
async()函数可以接受一个顶层函数作为参数,并与调用函数同时或并行地运行该函数。在这种情况下,没有线程,只有一个调用函数和一个被调用的函数作为async()函数的参数。异步函数的简单语法是。
future futObj = async(fn, fnArgs)
async函数返回一个未来对象。这里的第一个参数,对于async函数来说,是顶层函数的名称。在这之后可以有多个参数。其余的参数是顶层函数的参数。
如果顶层函数返回一个值,那么这个值将是未来对象的一个成员。而这是模仿线程返回值的一种方式。
future::get()
这个异步函数返回一个future对象。这个future对象拥有作为异步函数参数的函数的返回值。为了获得这个值,必须使用future对象的get()成员函数。情况是这样的。
future futObj = async(fn, fnArgs);
Type futObj.get();
当get()函数被调用时,调用函数的主体会等待(阻塞),直到异步函数返回其值。之后,get()语句下面的其他语句继续执行。
下面的程序与上述程序相同,没有正式创建线程,并且用get()语句代替了join语句。async()函数被用来模拟一个线程。两个顶层函数被减少为一个。该程序是
#include <iostream>
#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;
}
注意,已经包含了future库,而不是线程库。输出结果是。
Yes. So, be it!
结论
当涉及到一个连接语句时,涉及到两个顶层函数。一个是调用函数,另一个是被调用函数。在调用函数的主体中是连接语句。这两个函数可以分别被封装在一个线程中。被调用线程的 join() 成员函数在调用线程的主体中。当join()函数被调用时,调用线程在该点等待(阻塞),直到被调用线程完成;然后才继续操作。
通过使用future库中的async()函数可以避免对线程的使用。该函数将一个顶级函数作为参数,并返回一个future对象,该对象包含了async()函数的参数函数的返回值。为了获得作为参数的函数的返回值,必须使用future对象的get()成员函数。当get()成员函数被调用时,调用的函数体在这一点上等待(阻塞),直到被调用的函数完成;才继续操作。