介绍
1. 什么是线程池
线程池是一种多线程编程技术,用于管理和调度线程的集合。它包含了一组预先创建的线程,这些线程可以执行任务并处理工作队列中的任务。线程池的主要目的是提高线程的重用性和效率,以及降低线程创建和销毁的开销。
例如:
线程池就如公司,线程就是打工仔,任务就是工作,公司不会等到有工作才去招人,招人消耗的时间精力就是开销,打工仔排队等待上面下发工作(任务队列),接收到工作后去完成工作。
2. 线程池主要的组件构成:
线程池管理器(Thread Pool Manager):负责创建、销毁和管理线程池中的线程。它维护了线程池的状态、线程的数量、任务队列等信息,并根据需要动态地调整线程池中的线程数量。
工作队列(Work Queue):用于存储待执行的任务。线程池中的线程会从工作队列中获取任务,并执行任务的具体逻辑。工作队列可以是先进先出(FIFO)队列、优先级队列或其他类型的队列。
任务接口(Task Interface):定义了任务的执行逻辑。通常是一个函数或函数对象,线程池中的线程会调用任务接口来执行任务。任务接口可以是一个抽象类或接口,具体的任务可以是继承自该接口的类或者实现了该接口的对象。
线程池中的线程(Threads in the Thread Pool):由线程池管理器创建和管理的一组线程。这些线程会不断地从工作队列中获取任务,并执行任务的具体逻辑。线程池中的线程可以重复使用,避免了线程的频繁创建和销毁。
3. 线程池的优点包括:
减少线程创建和销毁的开销,提高了性能。
控制并发线程数量,避免系统资源被过度占用。
提高了线程的重用性和效率,使得线程的管理更加方便和灵活。
实现
1. 代码,最好要禁用拷贝构造和等于号
#include<iostream>
#include<thread>
#include<mutex>
#include<string>
#include<condition_variable>
#include<queue>
#include<vector>
#include<functional>
class ThreadPool{
public:
ThreadPool(int numThreads) : stop(false) { // **stop** 使用成员初始化列表可以在对象构造之前对成员变量进行初始化,而不是在构造函数体内对成员变量赋值。这样可以避免一些潜在的问题,提高了代码的效率和可读性。
for(int i = 0; i < numThreads; i++){
threads.emplace_back([this]{ //**避免push_back调用拷贝构造** // 捕获this是为了在函数中可以使用成员变量
while(1){
std::unique_lock<std::mutex> lock(mtx);
condition.wait(lock, [this]{
return !tasks.empty() || stop;
});
if(stop && tasks.empty()) {
return;
}
std::function<void()> task(std::move(tasks.front()));
tasks.pop();
lock.unlock();
task();
}
});
}
}
~ThreadPool(){
{
std::unique_lock<std::mutex> lock(mtx);
stop = true;
}
condition.notify_all();
for(auto& t : threads) {
t.join();
}
}
template<class F, class... Args>
void enqueue(F && f, Args&&... args){ // **&&万能引用,右值引用和左值引用**
std::function<void()>task = std::bind(std::forward<F>(f), std::forward<Args>(args)...); // **完美转发forward**
{ //确定作用域
std::unique_lock<std::mutex> lock(mtx);
tasks.emplace(std::move(task)); //队列不分前后不是emplace_back
}
condition.notify_one();
}
private:
std::vector<std::thread> threads; // 线程
std::queue<std::function<void()>> tasks; //任务队列
std::mutex mtx;
std::condition_variable condition; //生产者过来加任务了,就要通过条件变量去通知线程数组里面哪些线程去完成任务
bool stop;
};
int main(){
ThreadPool pool(4);
for(int i = 0; i < 10; i++){
pool.enqueue([i]{
std::cout <<"task : " << i << " is runing" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout <<"task : " << i << " is done" << std::endl;
});
}
return 0;
}
代码解释
1. ThreadPool(int numThreads) : stop(false)
-
构造函数
ThreadPool(int numThreads) : stop(false) {}
:-
这种方式使用成员初始化列表来初始化 stop 成员变量,将 stop 初始化为 false。
-
使用成员初始化列表可以在对象构造之前对成员变量进行初始化,而不是在构造函数体内对成员变量赋值。这样可以避免一些潜在的问题,提高了代码的效率和可读性。
-
-
构造函数
ThreadPool(int numThreads) { stop = false; }
:-
这种方式是在构造函数体内对 stop 成员变量进行赋值操作。
-
相比于成员初始化列表,这种方式会先调用默认构造函数创建 stop 对象,然后再将 false 赋值给 stop。这样做可能会导致不必要的构造和赋值操作,效率略低。
-
stop 使用成员初始化列表可以在对象构造之前对成员变量进行初始化,而不是在构造函数体内对成员变量赋值。这样可以避免一些潜在的问题,提高了代码的效率和可读性。
2.threads.emplace_back([this]
- 避免push_back调用拷贝构造(浅拷贝)
为什么要禁用拷贝构造
- 资源管理类:类中包含对资源(如内存、文件句柄等)的管理,如果允许拷贝,可能导致资源的重复释放或者多个对象共享同一资源的问题。禁用拷贝构造函数和拷贝赋值运算符可以确保资源的正确释放和管理。
- 单例模式类:单例模式类只允许存在一个实例,如果拷贝对象,则会破坏单例模式的约束。禁用拷贝构造函数和拷贝赋值运算符可以确保只有一个实例存在。
- 不可复制的类:有些类的设计本身就不支持拷贝,例如包含有指针成员的类,如果直接拷贝会导致指针指向的内存被多次释放。禁用拷贝构造函数和拷贝赋值运算符可以避免这种情况。
- 提高效率:有些类的拷贝构造函数和拷贝赋值运算符的实现比较复杂,禁用它们可以提高类的效率,因为不需要额外的复制操作。
线程中为什么要禁用拷贝构造
-
资源管理:线程池通常需要管理一组线程和任务队列等资源。如果允许拷贝线程池实例,可能会导致多个线程池实例共享同一组资源,这可能会导致资源管理混乱,例如任务被重复执行或者线程被重复创建。
-
线程安全性:线程池的设计旨在提供一种线程安全的任务执行环境。如果允许拷贝线程池实例,可能会导致多个线程同时操作同一组资源,从而破坏线程安全性。
-
单一责任原则:线程池通常被设计为一个独立的任务执行单元,负责管理自己的资源和任务。允许拷贝线程池实例可能会导致一个线程池实例承担多个任务执行单元的责任,违反了单一责任原则。
-
性能考虑:拷贝线程池实例可能涉及到复制大量资源或者状态,这可能会影响性能。禁止拷贝可以避免不必要的性能损耗。
-
浅拷贝:简单的赋值拷贝操作
-
深拷贝:在堆区重新申请空间,进行拷贝操作
-
就是两个变量共享同一数据,(&a == &b), 两个的地址相同;
3.[this]————lambda 表达式
捕获this是为了在函数中可以使用成员变量,类似于传参。
Lambda 表达式是 C++11 引入的一种函数对象,它可以用来创建匿名函数。Lambda 表达式的语法形式简洁明了,可以方便地在代码中编写轻量级的函数功能,通常用于函数对象参数、STL 算法等需要函数对象的场合。
-
Lambda 表达式的基本语法形式如下:
[capture](parameters) -> return_type { body }
- 其中,
capture
用于捕获外部变量,parameters
是参数列表,return_type
是返回类型,body
是函数体。
int n = 1; auto sum = [n](int a, int b) -> int { return a + b + n; }; int result = sum(1, 2); // result = 4;
- 其中,
4.void enqueue(F && f, Args&&... args)
"&&"
万能引用; 右值引用和左值引用
-
左值引用(左值可以被取地址):
- 左值引用可以绑定到左值(可以取地址的表达式)。
- 左值引用通常用来扩展变量的生命周期,例如函数参数传递和返回值。
- 左值引用使用 & 符号声明。
int x = 10; int& lref = x; // 左值引用绑定到左值 x
-
右值引用(右值不能被取地址):
- 右值引用可以绑定到临时对象和无法取地址的表达式,例如返回右值的函数调用。
- 右值引用通常用于移动语义,提高性能和避免不必要的拷贝。
- 右值引用使用 && 符号声明。
int&& rref = 10; // 右值引用绑定到右值 10
-
万能引用(universal reference):
- 万能引用是 C++11 中引入的一种引用类型,用于模板中,结合类型推导使用。
- 当一个模板参数被推导为引用类型时,并且形参使用了 T&& 的形式(T 是模板参数),则这种引用被称为万能引用。
- 万能引用可以绑定到左值或右值,具有引用折叠的特性。
template<typename T> void foo(T&& t) { // t 是万能引用,可以绑定到左值或右值 } int x = 10; foo(x); // T 被推导为 int&,t 是左值引用 foo(10); // T 被推导为 int,t 是右值引用
-
总结:
- 左值引用用于绑定左值,右值引用用于绑定右值,万能引用可以绑定到左值或右值,具有更广泛的适用性。
- 右值引用通常用于移动语义,而左值引用和万能引用通常用于模板编程和泛型编程。
5.完美转发forward
void enqueue(F && f, Args&&... args){
std::function<void()>task = std::bind(std::forward<F>(f), std::forward<Args>(args)...);
}
完美转发(perfect forwarding) 是 C++11 引入的一项功能,允许将参数按原始类型传递,即将参数传递给另一个函数,同时保留其值类别(左值或右值)。这在编写通用代码时非常有用,可以确保传递参数的原始值类别不变。相当于绑定函数参数,看看下面示例代码:
#include<iostream>
#include<functional>
int func(int a, int b, int c){
std::cout << "a: " << a << ", b: " << b << ", c: " << c << std::endl;
return a + b + c;
}
template<class F, class... Args>
void enqueue(F && f, Args&&... args){
// 将参数绑定,一一对应,task 接收返回值
std::function<int()> task = std::bind(std::forward<F>(f), std::forward<Args>(args)...);
task();// 执行函数
}
int main(){
enqueue(func, 0, 1, 2); // 运行结果:a: 0, b: 1, c: 2
}
所有的人生难题,都将在成长中找到答案。