线程池是一组线程,每个线程都有一种要执行的任务。因此,不同的线程执行不同种类的任务。因此,每个线程都有其专门的任务。一个任务基本上就是一个函数。类似的功能由一个特定的线程来完成;不同的类似功能集由另一个线程来完成,以此类推。尽管一个执行中的线程执行一个顶层函数,但根据定义,线程是线程类中一个对象的实例化。不同的线程有不同的参数,所以一个特定的线程应该参加一组类似的函数。
在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"。这个初始顺序没有什么问题。这就是并发操作的方式,不规则的。 其余的输出字符串在其函数被调用时出现。
在主函数体加入主线程后,它等待着主线程的完成。为了让主线程完成,所有的队列都必须是空的。每个队列的值都对应于其相应线程的执行。因此,对于每个队列成为空,其线程必须执行该次数;队列中有元素。
当主线程及其线程被执行并结束后,主函数继续执行。并显示:"程序已结束。"
结论
一个线程池是一组线程。每个线程负责执行自己的任务。任务就是函数。从理论上讲,任务总是在不断地进行。它们并没有真正结束,正如上面的例子所说明的那样。在一些实际的例子中,数据在线程之间是共享的。为了共享数据,程序员需要了解条件变量、异步函数、承诺和未来。这是在其他时间的讨论。