C++线程池

线程池

参考知乎轻松掌握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)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值