基于C++11以及17实现线程池【如何设置线程池大小】
单任务队列线程池
用现代的C++标准库(线程+锁+条件变量)实现一个单任务队列的线程池非常简单。基本的实现思路是:在线程池构造时初始化线程数,在析构时停止线程池。对外只需要提供提交任务的接口即可。
接口设计
返回类型
1 2 3 4 | explicit ThreadPool( size_t threads = std:: thread ::hardware_concurrency()); // 构造函数 template < typename F, typename ... Args> auto enqueue(F &&f, Args &&...args); // 入队接口 |
入队接口enqueue()
这个模板函数返回值使用了auto
关键字进行推导,实际上的返回值其实是一个future。
输入参数
输入参数是一个可调用对象和它的参数,这里利用了C++11的可变参数模板来实现传递任意数量的可调用对象的参数。
基本实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | class ThreadPool { public : explicit ThreadPool( size_t threads = std:: thread ::hardware_concurrency()); template < typename F, typename ... Args> auto enqueue(F &&f, Args &&...args); ~ThreadPool(); private : std::vector<std:: thread > workers; std::queue<std::function< void ()>> tasks; std::mutex queue_mutex; std::condition_variable condition; bool stop; }; |
注意:std::thread::hardware_concurrency()
在新版C++标准库中是一个很有用的函数。这个函数将返回能同时并发在一个程序中的线程数量。例如,多核系统中,返回值可以是CPU核芯的数量。返回值也仅仅是一个提示,当系统信息无法获取时,函数也会返回0。但是,这也无法掩盖这个函数对启动线程数量的帮助。
这个简单任务队列线程池的成员只有一个线程组,一个任务队列。为了保证任务队列的线程安全,提供一个锁。同时提供了一个条件变量,利用锁和条件变量,可以实现线程通知机制。线程通知机制指,刚开始时线程池中是没有任务的,所有的线程都等待任务的到来,当一个任务进入到线程池中,就会通知一个线程去处理到来的任务。
同时又提供一个stop变量,用来在析构的时候停止和清理任务和线程。因为懒(高情商:RAII风格线程池,生命周期基本上与应用的生命周期一致),没有提供stop接口。
下面是具体实现(C++11版本):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 | #ifndef THREAD_POOL_H #define THREAD_POOL_H #include <vector> #include <queue> #include <memory> #include <thread> #include <mutex> #include <condition_variable> #include <future> #include <functional> #include <stdexcept> class ThreadPool { public : ThreadPool( size_t ); template < class F, class ... Args> auto enqueue(F&& f, Args&&... args) -> std::future< typename std::result_of<F(Args...)>::type>; ~ThreadPool(); private : // need to keep track of threads so we can join them std::vector< std:: thread > workers; // the task queue std::queue< std::function< void ()> > tasks; // synchronization std::mutex queue_mutex; std::condition_variable condition; bool stop; }; // the constructor just launches some amount of workers inline ThreadPool::ThreadPool( size_t threads) : stop( false ) { for ( size_t i = 0;i<threads;++i) workers.emplace_back( [ this ] { for (;;) { std::function< void ()> task; { std::unique_lock<std::mutex> lock( this ->queue_mutex); this ->condition.wait(lock, [ this ]{ return this ->stop || ! this ->tasks.empty(); }); if ( this ->stop && this ->tasks.empty()) return ; task = std::move( this ->tasks.front()); this ->tasks.pop(); } task(); } } ); } // add new work item to the pool template < class F, class ... Args> auto ThreadPool::enqueue(F&& f, Args&&... args) -> std::future< typename std::result_of<F(Args...)>::type> { using return_type = typename std::result_of<F(Args...)>::type; auto task = std::make_shared< std::packaged_task<return_type()> >( std::bind(std::forward<F>(f), std::forward<Args>(args)...) ); std::future<return_type> res = task->get_future(); { std::unique_lock<std::mutex> lock(queue_mutex); // don't allow enqueueing after stopping the pool if (stop) throw std::runtime_error( "enqueue on stopped ThreadPool" ); tasks.emplace([task](){ (*task)(); }); } condition.notify_one(); return res; } // the destructor joins all threads inline ThreadPool::~ThreadPool() { { std::unique_lock<std::mutex> lock(queue_mutex); stop = true ; } condition.notify_all(); for (std:: thread &worker: workers) worker.join(); } #endif |
那么,如果我们要将上面的版本改造成C++17的版本,
Notable changes(显著的变化):
std::result_of
has been deprecated in C++ 17. Usestd::invoke_result
instead.- We should use
std::invoke
instead of writef(args...)
directly. - We could use initializer in lambda's capture list in C++ 14, so instead of
make_shared<std::packaged_task>
and copying it into the lambda we can write[task = std::move(task)]
.
完整的代码(C++17版本,参考自:https://github.com/jhasse/ThreadPool):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 | #ifndef THREAD_POOL_HPP #define THREAD_POOL_HPP #include <functional> #include <future> #include <queue> class ThreadPool { public : explicit ThreadPool( size_t ); template < class F, class ... Args> decltype ( auto ) enqueue(F&& f, Args&&... args); ~ThreadPool(); private : // need to keep track of threads so we can join them std::vector< std:: thread > workers; // the task queue std::queue< std::packaged_task< void ()> > tasks; // synchronization std::mutex queue_mutex; std::condition_variable condition; std::condition_variable condition_producers; bool stop; }; // the constructor just launches some amount of workers inline ThreadPool::ThreadPool( size_t threads) : stop( false ) { for ( size_t i = 0;i<threads;++i) workers.emplace_back( [ this ] { for (;;) { std::packaged_task< void ()> task; { std::unique_lock<std::mutex> lock( this ->queue_mutex); this ->condition.wait(lock, [ this ]{ return this ->stop || ! this ->tasks.empty(); }); if ( this ->stop && this ->tasks.empty()) return ; task = std::move( this ->tasks.front()); this ->tasks.pop(); if (tasks.empty()) { condition_producers.notify_one(); // notify the destructor that the queue is empty } } task(); } } ); } // add new work item to the pool template < class F, class ... Args> decltype ( auto ) ThreadPool::enqueue(F&& f, Args&&... args) { using return_type = std::invoke_result_t<F, Args...>; std::packaged_task<return_type()> task( std::bind(std::forward<F>(f), std::forward<Args>(args)...) ); std::future<return_type> res = task.get_future(); { std::unique_lock<std::mutex> lock(queue_mutex); // don't allow enqueueing after stopping the pool if (stop) throw std::runtime_error( "enqueue on stopped ThreadPool" ); tasks.emplace(std::move(task)); } condition.notify_one(); return res; } // the destructor joins all threads inline ThreadPool::~ThreadPool() { { std::unique_lock<std::mutex> lock(queue_mutex); condition_producers.wait(lock, [ this ] { return tasks.empty(); }); stop = true ; } condition.notify_all(); for (std:: thread & worker : workers) { worker.join(); } } #endif |
使用示例:
1 2 3 4 5 6 7 8 | // create thread pool with 4 worker threads ThreadPool pool(4); // enqueue and store future auto result = pool.enqueue([]( int answer) { return answer; }, 42); // get result from future std::cout << result.get() << std::endl; |
如何设置线程池大小?
线程池大小的设置需要考虑三方面的因素,服务器的配置、服务器资源的预算和任务自身的特性。具体来说就是服务器有多少个CPU,多少内存,IO支持的最大QPS是多少,任务主要执行的是计算、IO还是一些混合操作,任务中是否包含数据库连接等的稀缺资源。线程池的线程数设置主要取决于这些因素。
-
CPU密集型任务
也叫计算密集型任务,这种类型大部分状况下,CPU使用时间远高于I/O耗时,计算要处理、许多逻辑判断,几乎没有I/O操作的任务就属于CPU密集型;如果是CPU密集型任务,频繁切换上下线程是不明智的,此时应该设置一个较小的线程数,比如CPU的数目加1;
-
IO密集型任务
IO密集型任务应配置尽可能多的线程,因为IO操作不占用CPU,不要让CPU闲下来,应加大线程数量,如配置两倍CPU个数+1。
-
混合型任务
对于混合型的任务,如果可以拆分,拆分成IO密集型和CPU密集型分别处理,前提是两者运行的时间是差不多的,如果处理时间相差很大,则没必要拆分了。
->配置线程池大小的原则-阻抗公式
线程池设置线程数与CPU计算时间和I/O操作时间的比例相关,一个配置线程池大小的原则——阻抗匹配原则,其经验公式为:
1 2 3 4 | N = CPU数量 P = CPU繁忙时间 / 总运行时间 // 0 < P <=1 T = 所需设置线程数 T = N / P |
参考:
https://github.com/lzpong/threadpool
https://github.com/progschj/ThreadPool
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 终于写完轮子一部分:tcp代理 了,记录一下
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理