使用C++11特性实现一个简单的线程池

2023-12-27 21:33:22

运用到的知识点:智能指针,单例模式,懒汉模式,多线程,右值引用,可调用对象,完美转发,可变列表。

代码如下(一些细节全在注释中):?

#include <iostream>
#include <memory>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <vector>
#include <string>
#include <functional>

// 线程池(管理线程)
// 生成者往任务队列中加任务,线程池去管理这个线程数组,
// 调用线程数组去任务队列中取任务去完成

class Threadpool {
public:
	// 获取线程池实例的静态方法
	// std::make_shared无法访问私有或受保护的构造函数,
	// 需要使用std::shared_ptr的构造函数来创建一个指向新Threadpool对象的共享指针
	// 由于析构函数是私有属性所以需要手动指定删除器
	static std::shared_ptr<Threadpool> getInstance(int numThreads) {
		static std::shared_ptr<Threadpool> instance(new Threadpool(numThreads), [](Threadpool* p) {
			delete p;
		});
		return instance;
	}

	// 加任务
	// 由于传的参数个数不确定所以使用模板
	// 可变列表
	// 在函数模板中右值引用就是万能引用
	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)...);
		// 注意加锁的范围
		{
			std::unique_lock<std::mutex> loc(mtx);// 对共享变量一定不要忘记加锁
			// 使用move函数是为了将对象的所有权转移给另一个对象,
			// 避免不必要的拷贝和提高性能
			tasks.emplace(std::move(task));
		}

		// 既然加任务了,所以要通知线程来取任务
		condition.notify_one();
	}

private:
	// 私有化构造函数,单例模式的需要
	Threadpool(int numThreads) : stop(false) {
		for (int i = 0; i < numThreads; ++i) {
			// 不使用push_back的原因是会进行拷贝构造
			// 根本的原因是thread不支持拷贝构造
			threads.emplace_back([this] {
				// 加循环的作用:
				// 总结来说,while(1) 循环在这个线程池构造函数中的作用是:
				// 让每个工作线程在其生命周期内持续地处理任务。
				// 线程会在有任务可执行时被唤醒,
				// 当没有任务且收到停止信号时退出循环,从而优雅地结束其执行。
				// 这种设计使得线程池可以高效地管理和执行多个任务,
				// 而不需要频繁地创建和销毁线程。
				while (1) {
					std::unique_lock<std::mutex> lock(mtx);
					// 满足条件就继续执行,否则阻塞
					condition.wait(lock, [this] {
						// 任务队列不为空就不阻塞
						// 线程终止就继续执行到线程终止的语句
						return !tasks.empty() || stop;
					});

					if (stop && tasks.empty()) return;// 线程终止就结束函数

					// 若到现在仍然可以执行到这里说明可以正常的取任务
					// 可以使用auto自动推导类型
					std::function<void()> task(std::move(tasks.front()));
					tasks.pop();// 任务出队列
					lock.unlock();// 已经取到任务了就让其他线程去取任务
					task();// 调用该线程取到的任务
				}
			});
		}
	}

	~Threadpool() {
		{
			//只要是多个线程访问的函数就需要加锁
			std::unique_lock<std::mutex> lock(mtx);
			stop = true;
		}
		// 现在stop为真,所以通知所有线程工作
		// 将任务队列中的所有任务都取完
		condition.notify_all();
		// 使用join()的时候不能使用const关键字
		// 因为join()本身就是一个修改操作
		for (auto& t : threads) {
			t.join();
		}
	}

	// 禁止拷贝和赋值,单例模式的需要
	Threadpool(const Threadpool&) = delete;
	Threadpool& operator=(const Threadpool&) = delete;

	std::vector<std::thread> threads;// 线程数组
	std::queue<std::function<void()>> tasks;// 任务队列的参数当然是函数
	std::mutex mtx;// 互斥锁
	std::condition_variable condition;// 条件变量
	bool stop;// 线程池什么时候终止
};

int main() {
	// 使用单例模式获取线程池实例
	std::shared_ptr<Threadpool> pool = Threadpool::getInstance(4);

	// 加任务
	for (int i = 0; i < 10; ++i) {
		pool -> enqueue([i] {
			std::cout << "task:" << i << " is running " << std::endl;
			std::this_thread::sleep_for(std::chrono::seconds(1));
			std::cout << "task:" << i << " is end " << std::endl;
		});
	}

	// 主线程可以等待一些时间,否则主线程可能在任务完成前结束
	std::this_thread::sleep_for(std::chrono::seconds(12));

	return 0;
}

输出结果不唯一:

task:0 is running
task:3 is running
task:2 is running
task:1 is running
task:3 is end
task:2 is end
task:5 is running
task:0 is end
task:1 is end
task:7 is running
task:6 is running
task:4 is running
task:4 is end
task:5 is end
task:9 is running
task:6 is end
task:7 is end
task:8 is running
task:9 is end
task:8 is end

文章来源:https://blog.csdn.net/qq_73185160/article/details/135250391
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。