什么是线程池?
当我们遇到耗费时间的任务却又不想主线程阻塞在这里,就可以考虑使用多线程提高效率。c++11的thread为多线程提供了支持。线程在std::thread对象创建时启动,需要传入一个函数名。这个函数在其所属线程上运行,直到函数执行完毕,线程结束。
启动线程时,需要明确指定是要等待线程结束,还是让其自主运行。如果std::thread对象销毁前还没有做出决定,线程就会终止(std:thread的析构函数会调用std::terminate())
-
加入式
void hello() { cout << "hello, world" << endl; } int main() { thread t(hello); t.join(); cout << "me, too" << endl; return 0; } //输出结果唯一; //hello, world //me, too -
分离式
void hello() { cout << "hello, world" << endl; } int main() { thread t(hello); t.detach(); cout << "me, too" << endl; return 0; } //以下结果都有可能; //1 //me, toohello, world //2 //me, too //hello, world如果不等待线程,就必须保证线程结束前,可访问的数据的有效性。在使用多线程的时候,尽量使线程函数的功能齐全,将数据复制到线程中,而非复制到共享数据中。
但如果遇到一个任务我们就创建一个线程,就会浪费大量的时间在线程的创建和销毁上,毕竟创建线程是需要系统调用进入内核态的。所以我们在一开始就批量创建线程,也就是线程池,通过复用线程来节省开销。一个线程池大致结构如下:
主线程和N个工作线程类似单生产者/多消费者模型,它们通过一个任务队列来交互。主线程调用
addTask往队列中添加任务,N个工作线程从队列中取出任务并执行。
有哪些问题?
基本实现如下:
#include <mutex>
#include <vector>
#include <queue>
#include <condition_variable>
#include <thread>
#include <functional>
class threadPool_v1 {
public:
threadPool_v1(int threads_num):stop(false), threads(std::vector<std::thread>(threads_num)){
}
~threadPool_v1() = default;
///拷贝/移动构造函数,拷贝/移动赋值运算符均设置为delete
threadPool_v1(const threadPool_v1&) = delete;
threadPool_v1(threadPool_v1&&) = delete;
threadPool_v1& operator=(const threadPool_v1&) = delete;
threadPool_v1& operator=(threadPool_v1&&) = delete;
/// 开启多个线程
void init() {
for(int i = 0; i < threads.size(); i++) {
threads[i] = std::thread(std::bind(&threadPool_v1::run, this));
}
}
void shutdown() {
stop = true;
//必须加上,不然线程阻塞在空队列上
cond.notify_all();
for(int i = 0; i < threads.size(); i++) {
if(threads[i].joinable()) {
threads[i].join();
}
}
}
bool addTask(std::function<void()> fun) {
std::unique_lock<std::mutex> lock(mtx);
if(stop) {
return false;
}
tasks.push(fun);
cond.notify_one();
return true;
}
void run() {
while(!stop) {
std::unique_lock<std::mutex> lock(mtx);
if(tasks.empty()) {
cond.wait(lock);
}
auto task = std::move(tasks.front());
tasks.pop();
lock.unlock();
if(task) {
task();
}
}
}
private:
std::mutex mtx;
std::condition_variable cond;
bool stop;
std::queue<std::function<void()>> tasks;
std::vector<std::thread> threads;
};
#endif//THREADPOOL__THREADPOOL_V1_H_
在main.cpp中测试代码如下:
void printNum() {
int num = random();
std::cout << num << std::endl;
}
int main() {
threadPool_v1 pool(2);
std::function<void()> func = printNum;
pool.init();
pool.addTask(printNum);
pool.addTask(printNum);
pool.addTask(printNum);
//等待任务执行完成
std::this_thread::sleep_for(std::chrono::seconds(2));//sleep 2秒
pool.shutdown();
std::cout << "shutdown" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(2));//sleep 2秒
}
那么我的实现有哪些需要注意的问题或者说改进的点么?
- thread对象创建的时候不可以设置为
join(),因为join会阻塞主线程,主线程会在创建第一个线程后阻塞,无法接着创建线程 - 这里的shutdown关闭线程池,虽然可以保证线程正在执行的任务可以结束,但是不能保证队列中的任务都执行
- 线程的工作任务只能以
std::function<>的形式传递,且无法传递有参数的任务
那么如何确保在主线程在工作线程之后销毁呢,可以修改线程池对象的析构函数,在析构函数中调用shutdown()函数即可。
不过上面的代码依然有一个问题,那就是如何传递含有参数的工作函数,每次调用之前都需要手动使用std::bind()未免有点麻烦了。参考开源的一个代码实现:
我们可以通过可变参数来解决上述问题,具体过程已在注释中解释了。
//这里是C++中的尾置返回,函数接受一个“万能引用”,左值/右值都可以接收,和一个可变参数模板
template<typename F, typename...Args>
auto submit(F&& f, Args&&... args) -> std::future<decltype(f(args...))> {
// 使用std::bind把传递进来的函数和参数绑定为一个新的可调用对象
// std::forward可以保持参数原本的类型
std::function<decltype(f(args...))()> func = std::bind(std::forward<F>(f), std::forward<Args>(args)...);
// 使用std::packaged_task包装一个可调用对象,可以异步的使用get_future()获取结果
auto task_ptr = std::make_shared<std::packaged_task<decltype(f(args...))()>>(func);
// 使用匿名函数将wrok_func包装成一个无参数,无返回值的可调用对象
std::function<void()> wrapper_func = [task_ptr]() {
(*task_ptr)();
};
// 入队
m_queue.enqueue(wrapper_func);
// 唤醒一个被阻塞的线程
m_conditional_lock.notify_one();
// 返回结果
return task_ptr->get_future();
}
在解决了线程池的实现之后,还剩下最后一个问题。线程池的线程数目设置为多少比较合适?根据阻抗匹配原则。如果池中线程在执行任务时,密集计算所占的时间比重为P,而系统一共有C个CPU,为了让这C个CPU跑满而又不至于过载,线程池大小的经验公式T = C/P。