学习如何在C++中创建一个线程池

134 阅读4分钟

线程池是一组线程,每个线程都有一种要执行的任务。因此,不同的线程执行不同种类的任务。因此,每个线程都有其专门的任务。一个任务基本上就是一个函数。类似的功能由一个特定的线程来完成;不同的类似功能集由另一个线程来完成,以此类推。尽管一个执行中的线程执行一个顶层函数,但根据定义,线程是线程类中一个对象的实例化。不同的线程有不同的参数,所以一个特定的线程应该参加一组类似的函数。

在C++中,这个线程池必须要被管理。C++没有一个用于创建线程池和管理的库。这可能是因为有不同的方法来创建一个线程池。因此,C++程序员必须根据需要创建一个线程池。

什么是线程?一个线程是一个从线程类中实例化出来的对象。在正常的实例化中,线程构造函数的第一个参数是一个顶层函数的名称。线程构造函数的其余参数是该函数的参数。当线程被实例化时,该函数开始执行。C++ main()函数是一个顶层函数。该全局范围内的其他函数也是顶层函数。碰巧的是,main()函数是一个线程,它不像其他线程那样需要正式声明。考虑一下下面的程序。

  #include <iostream>
    #include <thread>
    using namespace std;

    void func() {
        cout << "code for first output" << endl;
        cout << "code for second output" << endl;
    }

    int main()
    {
        thread thr(func);
        thr.join();
        /* other statements */

        return 0;
    }

输出的结果是:

 code for first output
 code for second output

注意包含了拥有线程类的线程库。func()是一个顶层函数。main()函数中的第一条语句在线程的实例化中使用了它,即three。main()中的下一条语句是一个连接语句。它将线程thr连接到main()函数线程的主体中,在它被编码的位置。如果没有这条语句,主函数可能会在线程函数完成之前执行完毕。这意味着麻烦。

对于g++编译器来说,应该使用类似以下的命令来运行一个C++20的线程程序。

    g++ -std=c++2a temp.cpp -lpthread -o temp

这篇文章解释了在C++中创建和管理线程池的一种方法。

文章内容

线程池示例要求

这个示例性线程池的要求很简单。有三个线程和一个主线程。这些线程都从属于主线程。每个从属线程都使用一个队列数据结构。因此有三个队列:qu1、qu2和qu3。队列库和线程库都必须包含在程序中。

每个队列可以有一个以上的函数调用,但都是同一顶层的函数。也就是说,一个队列的每个元素都是为了调用一个特定的顶层函数。因此,有三个不同的顶层函数:每个线程有一个顶层函数。这些函数的名称是fn1、fn2和fn3。

每个队列的函数调用只在其参数上有所不同。为了简单起见,在这个程序例子中,函数调用将没有参数。事实上,本例中每个队列的值将是同一个整数:1作为所有qu1元素的值;2作为所有qu2元素的值;3作为所有qu3元素的值。

一个队列是一个先入先出的结构。所以第一个进入队列的调用(号码)是第一个离开的。当一个调用(数字)离开时,相应的函数及其线程被执行。

main()函数负责向三个队列中的每一个队列提供相应的函数调用,因此有相应的线程。

主线程负责检查任何队列中是否有调用,如果有调用,则通过其线程调用相应的函数。在这个程序例子中,当没有队列有任何线程时,程序就结束了。

顶层函数很简单,对于这个教学实例,它们是。

 void fn1() {
        cout << "fn1" << endl;
    }

    void fn2() {
        cout << "fn2" << endl;
    }

    void fn3() {
        cout << "fn3" << endl;
    }

相应的线程将是thr1、thr2和thr3。主线程有自己的主函数。这里,每个函数只有一条语句。函数fn1()的输出是 "fn1"。函数fn2()的输出是 "fn2"。函数fn3()的输出是 "fn3"。

在本文的最后,读者可以把本文中所有的代码段放在一起,形成一个线程池程序。

全局变量

带有全局变量的程序顶部,是:

 #include <iostream>
    #include <thread>
    #include <queue>
    using namespace std;

    queue<int> qu1;
    queue<int> qu2;
    queue<int> qu3;

    thread thr1;
    thread thr2;
    thread thr3;

队列和线程变量是全局变量。它们被声明时没有进行初始化或声明。在这之后,在程序中,应该是三个从属的顶层函数,如上图所示。

iostream库是为cout对象而包含的。线程库是为线程而设的。这些线程的名字是thr1、thr2和thr3。队列库包括了队列。队列的名称是qu1、qu2和qu3。qu1对应于thr1;qu2对应于thr2,qu3对应于thr3。一个队列就像一个矢量,但它是为先进先出(first_in-first_out)服务的。

主线程功能

在这三个从属的顶级函数之后,是程序中的主函数。它是:

void masterFn() {
        work:
        if (qu1.size() > 0) thr1 = thread(fn1);
        if (qu2.size() > 0) thr2 = thread(fn2);
        if (qu3.size() > 0) thr3 = thread(fn3);

        if (qu1.size() > 0) {
            qu1.pop();
            thr1.join();
        }
        if (qu2.size() > 0) {
            qu2.pop();
            thr2.join();
        }
        if (qu3.size() > 0) {
            qu3.pop();
            thr3.join();
        }

        if (qu1.size() == 0 && qu1.size() == 0 && qu1.size() == 0)
            return;
        goto work;
    }

goto-loop体现了该函数的所有代码。当所有的队列都是空的时候,该函数返回无效,语句为 "return;"

goto-loop的第一个代码段有三个语句:每个队列和相应的线程都有一个。在这里,如果一个队列不是空的,它的线程(和相应的下级顶层函数)将被执行。

下一个代码段由三个if-结构组成,每个结构对应一个下级线程。每个if结构有两条语句。第一条语句删除数字(用于调用),这可能发生在第一个代码段。接下来是一个连接语句,它确保相应的线程工作到完成。

goto-loop中的最后一条语句结束函数,如果所有的队列都是空的,就走出循环。

Main()函数

在程序中的主线程函数之后,应该是main()函数,其内容为:

    qu1.push(1);
        qu1.push(1);
        qu1.push(1);

        qu2.push(2);
        qu2.push(2);

        qu3.push(3);

        thread masterThr(masterFn);
        cout << "Program has started:" << endl;
        masterThr.join();
        cout << "Program has ended." << endl;

main()函数负责将代表调用的数字放入队列。Qu1有三个值为1;qu2有两个值为2,qu3有一个值为3。 main()函数启动主线程,并将其加入到主体中。作者的计算机的一个输出是:

    Program has started:
    fn2
    fn3
    fn1
    fn1
    fn2
    fn1
    Program has ended.

输出显示了线程的不规则并发操作。在main()函数加入其主线程之前,它显示 "程序已开始:"。主线程依次调用thr1为fn1(),thr2为fn2(),thr3为fn3()。然而,相应的输出以 "fn2 "开始,然后是 "fn3",最后是 "fn1"。这个初始顺序没有什么问题。这就是并发操作的方式,不规则的。 其余的输出字符串在其函数被调用时出现。

在主函数体加入主线程后,它等待着主线程的完成。为了让主线程完成,所有的队列都必须是空的。每个队列的值都对应于其相应线程的执行。因此,对于每个队列成为空,其线程必须执行该次数;队列中有元素。

当主线程及其线程被执行并结束后,主函数继续执行。并显示:"程序已结束。"

结论

一个线程池是一组线程。每个线程负责执行自己的任务。任务就是函数。从理论上讲,任务总是在不断地进行。它们并没有真正结束,正如上面的例子所说明的那样。在一些实际的例子中,数据在线程之间是共享的。为了共享数据,程序员需要了解条件变量、异步函数、承诺和未来。这是在其他时间的讨论。