了解C++的连接线程

98 阅读2分钟

线程就像一个程序的子程序。因此,它们会独立运行,除非它们被加入。连接意味着一个线程将运行到某一点,然后停止并等待另一个线程完成其执行(到其结束);然后再继续自己的执行。在第一个线程停止的那一点上,有一个连接语句。一个线程可以调用另一个线程。调用的线程才是加入的。被调用的线程并没有加入。

加入之所以存在,是因为线程需要相互通信。在变化发生后,被调用的线程可能会改变一个全局变量的值,而调用的线程需要访问该变量。这是一种同步的形式。

本文解释了连接线程的两种方法。它以一个关于什么是线程的插图开始。

文章内容

线程

考虑一下下面的程序。

#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()成员函数被调用时,调用的函数体在这一点上等待(阻塞),直到被调用的函数完成;才继续操作。