C++11与简单线程池实现
膜拜了一篇github上高赞的线程池实现,其中用到了很多C++11的新特性。
不足百行的线程池代码,里面有很多值得学习、借鉴的思想和方法 (@_@~好学脸)
class noncopyable
{
public:
noncopyable(){}
~noncopyable(){}
private:
noncopyable(const noncopyable&);
const noncopyable& operator= (noncopyable&){}
};
定义一不可拷贝的类,将该类拷贝构造函数与赋值运算符的重载声明成私有。
threadPool类继承noncopyable
class threadpool : public noncopyable
首先是需要用到的成员变量
using Task = function<void()>; //using对标typedef比其功能多一点 它可用于模板
queue<Task> _tasks; //任务队列,先入队列的先执行(此数据结构 应结合应用场景)
vector<thread> _pool; //线程池
mutex _mutex; //互斥量 (同步)
condition_variable task_cv; //条件变量 (条件阻塞)
atomic_bool _isrun{true}; //原子化的bool (线程池是否执行)
atomic_int _curFreeThreadNum; //原子化的int (空闲的线程数)
在threadPool的构造函数中初始化线程池
#define MAX_THREAD_POOL_NUM 16 //池中最大线程数
threadpool(unsigned short size) //需要起多少线程
{
addthreadpool(size);
}
void addthreadpool(unsigned short size)
{
for (;size < MAX_THREAD_POOL_NUM && size > 0; --size)
{
_pool.emplace_back([this] // (1)emplace_back与push_back 见备注
{
while (_isrun) //运行中一直循环
{
Task task;
{
unique_lock<mutex> lock(_mutex);
task_cv.wait(lock, [this] // (2)条件变量condition_variable
{
return !_isrun || !_tasks.empty();
}); // wait 直到有 task
if (_tasks.empty() && !_isrun) //满足该条件退出
return;
task = move(_tasks.front()); //去除任务队列中前面的任务
_tasks.pop(); //出队列
}
_curFreeThreadNum++;
task(); //真正执行
_curFreeThreadNum--;
}
});
}
}
commit模板函数作用是将需要执行的方法push到任务队列中
template<class F, class... Args>
auto commit(F&& f, Args&&... args) ->future<decltype(f(args...))> (4)模板函数签名
{
if (!_run) // stoped ??
throw runtime_error("commit on ThreadPool is stopped.");
using RetType = decltype(f(args...)); // typename std::result_of<F(Args...)>::type, 函数 f 的返回值类型
auto task = make_shared<packaged_task<RetType()>>(
bind(forward<F>(f), forward<Args>(args)...) (5)packaged_task与forward
); // 把函数入口及参数,打包(绑定)
future<RetType> future = task->get_future();
{ // 添加任务到队列
lock_guard<mutex> lock{ _lock };//对当前块的语句加锁 lock_guard 是 mutex 的 stack 封装类,构造的时候 lock(),析构的时候 unlock()
_tasks.emplace([task](){ // push(Task{...}) 放到队列后面
(*task)();
});
}
#ifdef THREADPOOL_AUTO_GROW
if (_idlThrNum < 1 && _pool.size() < THREADPOOL_MAX_NUM)
addThread(1);
#endif // !THREADPOOL_AUTO_GROW
_task_cv.notify_one(); // 唤醒一个线程执行
return future;
}
线程池的析构
~threadpool()
{
_run=false;
_task_cv.notify_all(); // 唤醒所有线程执行
for (thread& thread : _pool) {
//thread.detach(); // 让线程“自生自灭”
if(thread.joinable())
thread.join(); // 等待任务结束, 前提:线程一定会执行完
}
}
结合调用总结下大体流程
threadpool executor{ 10 };
std::future<std::string> fh = executor.commit([]()->
std::string { std::cout << "hello, fh ! " <<
std::this_thread::get_id() <<
std::endl; return "hello,fh ret !"; });
std::cout << fh.get().c_str() << " "
<< std::this_thread::get_id() << std::endl;
还是比较清晰的,声明并初始化线程池,首先进入构造函数,添加特定数量线程,每个线程都会在_task_cv.wait时阻塞,并释放锁定(对mutex的);随后调用commit方法:将任务放入packaged_task打包,并塞进任务队列。_task_cv.notify_one()唤醒一个线程。这时queue<Task> _tasks已不为空,线程又被唤醒,因此任务会在一wait线程种继续执行(从队列中取出)。
最后离开作用域,析构被调用;_run=false 线程一定会被唤醒_task_cv.notify_all() 唤醒所有用此条件变量阻塞的线程thread.join() 等待任务结束 END。
备注
(1)emplace_back与push_back
emplace_back() 和 push_back() 的区别,就在于底层实现的机制不同。push_back() 向容器尾部添加元素时,首先会创建这个元素,然后再将这个元素拷贝或者移动到容器中(如果是拷贝的话,事后会自行销毁先前创建的这个元素);而 emplace_back() 在实现时,则是直接在容器尾部创建这个元素,省去了拷贝或移动元素的过程。
(2)条件变量condition_variable
std::condition_variable 是条件变量,更多有关条件变量的定义参考维基百科。Linux 下使用 Pthread 库中的 pthread_cond_*() 函数提供了与条件变量相关的功能, Windows 则参考 MSDN。
当 std::condition_variable 对象的某个 wait 函数被调用的时候,它使当前线程进入睡眠状态并自动释放锁。当前线程会一直被阻塞,直到另外一个线程在相同的 std::condition_variable 对象上调用了 notification 函数来唤醒当前线程。线程唤醒后,它重新获取线程进入睡眠状态时释放的锁定~
std::condition_variable 对象通常使用 std::unique_lock<std::mutex> 来等待,如果需要使用另外的 lockable 类型,可以使用 std::condition_variable_any 类,本文后面会讲到 std::condition_variable_any 的用法。
本线程池实现中 使用了其中的一个wait函数,当匿名函数返回false时 才会有等待的可能
template <class _Predicate>
void wait(unique_lock<mutex>& _Lck, _Predicate _Pred) { // wait for signal and test predicate
while (!_Pred()) { //(3)虚假唤醒
wait(_Lck);
}
}
(3)虚假唤醒
源码中有while(!_Pred())已经杜绝了虚假唤醒(spurious wakeup)
即使没有线程调用condition_signal, 原先调用condition_wait的函数也可能会返回。此时线程被唤醒了,但是条件并不满足,这个时候如果不对条件进行检查而往下执行,就可能会导致后续的处理出现错误。
(4)模板函数签名
该模板表明可以传入多形参的方法,“->”返回值后置(放在前面并不知是何类型),decltype自动推导返回值类型。这样写模板简直骚气到不行
(5)packaged_task与forward
std::packaged_task 包装一个可调用的对象,并且允许异步获取该可调用对象产生的结果。包装对象的执行结果传递给了std::future对象。本例中task是异步执行的,不会立刻执行
std::forward 实现了完美转发即左右值属性强制不发生变化
文章参考: http://c.biancheng.net/view/6826.html
https://www.cnblogs.com/haippy/p/3284540.html