线程池
参考知乎轻松掌握C++线程池:从底层原理到高级应用 - 知乎 (zhihu.com)
概念
线程池是一种并发编程技术,能够有效地管理并发的线程、减少资源占用和提高程序性能。
优点
1.提高性能和资源利用率
线程池主要解决两个问题:线程创建和销毁的开销以及线程竞争造成的性能瓶颈。通过预先创建一组线程并复用他们,线程池有效地降低了线程创建和销毁的时间和资源消耗。同时,通过管理线程并发数量,线程池有助于减少线程之间的竞争,增加资源利用率,并提高程序运行的性能。
2.减少创建和销毁线程的开销
3.线程竞争问题解决
过多的线程可能导致线程竞争,影响系统性能。线程池通过维护一个可控制的并发数量,有助于减轻线程之间的竞争。例如,当CPU密集型任务和I/O密集型任务共存时,可以通过调整线程资源,实现更高效的负载平衡。
线程池的工作原理
线程池通过预先创建和调度复用线程来实现资源优化。这个过程包括:创建线程、任务队列与调度、以及线程执行和回收。
1.创建线程
初始化线程池构造函数时,此时会阻塞在this->condition.wait
直至线程池终止或者有任务进入线程池。
ThreadPool(int numsThreads) : stop(false){
for(int i = 0; i < numsThreads; i++){
workerThreads.emplace_back([this]{
while(true){
function<void()> task;
{
unique_lock<mutex> lock(this->queueMutex);
//程序执行到condition.wait的时候,会阻塞
//直到stop为true也就是线程池结束,或者有任务进入线程池
this->condition.wait(lock,[this]{
return this->stop || !this->tasks.empty();
});
if(this->stop && this->tasks.empty())
return;
//将tasks队列中的首元素转移到task而不是简单的复制,并不是真正的移动,而是将其转换为右值引用
task = move(this->tasks.front());
this->tasks.pop();
}
task();
}
});
}
}
2.任务队列与调度
线程池通过维护一个任务队列来管理待执行任务。当线程池收到一个新任务时,它会将任务加入到任务队列中。线程会按照预定策略(例如FIFO)从队列中取出任务执行。
template<class F, class...Args>
void enqueue(F&& f, Args&&... args){
//bind创建一个绑定对象,将一个可调用对象(函数、函数对象、成员函数)与其参数绑定在一起,从而形成一个新的可调用对象
//forward用于实现完美转发,确保传递给bind参数保持其原始的值类别
auto task = bind(forward<F>(f), forward<Args>(args)...);
{
unique_lock<mutex> lock(this->queueMutex);
if(stop)
throw runtime_error("enqueue on stopped ThreadPool");
tasks.emplace(task);
}
//有新的任务进入队列,通知一个正在等待该条件变量上的线程继续执行
this->condition.notify_one();
}
3.线程执行和回收
完整代码
#include <iostream>
#include <vector>
#include <queue>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <functional>
using namespace std;
class ThreadPool{
public:
ThreadPool(int numsThreads) : stop(false){
for(int i = 0; i < numsThreads; i++){
workerThreads.emplace_back([this]{
while(true){
function<void()> task;
{
unique_lock<mutex> lock(this->queueMutex);
//程序执行到condition.wait的时候,会阻塞
//直到stop为true也就是线程池结束,或者有任务进入线程池
this->condition.wait(lock,[this]{
return this->stop || !this->tasks.empty();
});
if(this->stop && this->tasks.empty())
return;
//将tasks队列中的首元素转移到task而不是简单的复制,并不是真正的移动,而是将其转换为右值引用
task = move(this->tasks.front());
this->tasks.pop();
}
task();
}
});
}
}
template<class F, class...Args>
void enqueue(F&& f, Args&&... args){
//bind创建一个绑定对象,将一个可调用对象(函数、函数对象、成员函数)与其参数绑定在一起,从而形成一个新的可调用对象
//forward用于实现完美转发,确保传递给bind参数保持其原始的值类别
auto task = bind(forward<F>(f), forward<Args>(args)...);
{
unique_lock<mutex> lock(this->queueMutex);
if(stop)
throw runtime_error("enqueue on stopped ThreadPool");
tasks.emplace(task);
}
//有新的任务进入队列,通知一个正在等待该条件变量上的线程继续执行
this->condition.notify_one();
}
~ThreadPool(){
{
unique_lock<mutex> lock(this->queueMutex);
stop = true;
}
condition.notify_all();
for(thread& worker : workerThreads)
worker.join();
}
private:
vector<thread> workerThreads;
queue<function<void()>> tasks;
mutex queueMutex;
condition_variable condition;
bool stop;
}
小例子
这里以文件操作为例子演示,之所以以文件操作为例子,主要是为了后期解决QT网盘项目中的一个多线程实现多文件同时上传和下载的一个功能。Github链接为qlzhai/Qt-NetworkDisk at master (github.com)
定义线程任务为向txt文件中写内容。代码如下:
void fileOperator(int num) {
ofstream fout;
string str = "files\\file" + to_string(num) + ".txt";
fout.open(str);
for (int i = 0; i < 1000; i++) {
fout << "hello" + to_string(i) + "\n";
}
fout.close();
}
这里分别以单线程和多线程为例向1000个txt文件执行写操作。
需要说明的是,下面程序中用到了一个同步操作,采用finished
作为一个同步标志位。
#include <iostream>
#include <fstream>
#include <chrono>
#include "ThreadPool.h"
using namespace std;
std::mutex mtx;
std::condition_variable cv;
bool finished = false;
int main() {
// 获取程序开始时间点
auto start1 = std::chrono::high_resolution_clock::now();
int cores = std::thread::hardware_concurrency();//获取硬件所支持的最大线程数
ThreadPool pool(cores);
// 用于记录多线程任务的完成数量
int tasksCompleted = 0;
const int totalTasks = 1000;
cout << "多线程任务开始" << endl;
for (int i = 0; i < 1000; i++) {
pool.enqueue([i, &tasksCompleted, totalTasks] {
ofstream fout;
string str = "files\\file" + to_string(i) + ".txt";
fout.open(str);
for (int i = 0; i < 1000; i++) {
fout << "hello" + to_string(i) + "\n";
}
fout.close();
// 任务完成后增加计数
{
std::lock_guard<std::mutex> lock(mtx);
tasksCompleted++;
if (tasksCompleted == totalTasks) {
finished = true;
cv.notify_one(); // 唤醒等待的线程
}
}
});
}
// 等待所有多线程任务完成
{
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [] { return finished; });
}
// 获取程序结束时间点
auto end1 = std::chrono::high_resolution_clock::now();
// 计算时间差,并转换为毫秒
auto duration1 = std::chrono::duration_cast<std::chrono::milliseconds>(end1 - start1);
// 输出程序运行时间,单位毫秒
std::cout << "多线程程序运行时间: " << duration1.count() << " 毫秒" << std::endl;
// 获取程序开始时间点
auto start2 = std::chrono::high_resolution_clock::now();
cout << "单线程任务开始" << endl;
for (int i = 1000; i < 2000; i++) {
ofstream fout;
string str = "files\\file" + to_string(i) + ".txt";
fout.open(str);
for (int i = 0; i < 1000; i++) {
fout << "hello" + to_string(i) + "\n";
}
fout.close();
}
// 获取程序结束时间点
auto end2 = std::chrono::high_resolution_clock::now();
// 计算时间差,并转换为毫秒
auto duration2 = std::chrono::duration_cast<std::chrono::milliseconds>(end2 - start2);
// 输出程序运行时间,单位毫秒
std::cout << "单线程程序运行时间: " << duration2.count() << " 毫秒" << std::endl;
return 0;
}
程序运行结果为
多线程任务开始
多线程程序运行时间: 3447 毫秒
单线程任务开始
单线程程序运行时间: 7918 毫秒
很显然,多线程的效果要好很多。后期将会在QT网盘项目中实现一个多文件同时上传和下载的功能。Github链接为qlzhai/Qt-NetworkDisk at master (github.com)