ThreadPool
- 简述
一个线程池的simple implementation,包含很多modern c++的细节
- Source github.com/chenzhengda…
#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
#include <iostream>
#include <vector>
#include <chrono>
#include "ThreadPool.h"
int main()
{
ThreadPool pool(4);
std::vector< std::future<int> > results;
for(int i = 0; i < 8; ++i) {
results.emplace_back(
pool.enqueue([i] {
std::cout << "hello " << i << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "world " << i << std::endl;
return i*i;
})
);
}
for(auto && result: results)
std::cout << result.get() << ' ';
std::cout << std::endl;
return 0;
}
代码分析
ThreadPool类声明
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>;
/*
1.class 与 typename
2.可变参数模板(varadic template)
3.万能引用Universal References(此处应该不是右值引用)
4.auto + -> 模板返回值类型推导
5.std::future 异步机制
6.std::result_of + ::type 在编译时推导INVOKE表达式的类型
*/
~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;
};
函数需要在多线程中运行,但是每来一个函数就开启一个线程的方式缺点很多,所以需要固定的N个线程来跑执行,但是有的线程还没有执行完,有的又在空闲,如何分配任务呢,可以封装一个线程池来完成这些操作,有了线程池这层封装,你就只需要告诉它开启几个线程,然后直接塞任务就行了,然后通过一定的机制获取执行结果。
线程池的数据区(private),首先需要有一些工作线程workers
,需要有一个队列管理任务tasks
,为了实现线程间的异步逻辑,需要一把锁queue_mutex
,一个条件变量condition
来完成线程等待和唤醒(有task唤醒,无task等待)。
数据区都算好理解,比较复杂的是enqueue
这个函数,要素众多,都写在注释里
构造析构
// 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();
}
}
);
}
// 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();
}
- 构造函数在做什么?
在主线程中使用std::thread
创建几个子线程(创建好后塞入vector里),线程入口函数是一个lambda function,注意此时主线程把lambda塞完就继续往下走了,构造函数执行完了。
那我们放下主线程(假设主线程先sleep一会儿好了),先看看子线程去干吗了,子线程执行这个lambda function,这个lambda 做了什么呢?让子线程执行一个死循环,那循环里做了什么?首先某个先进来的子线程通过std::unique_lock
拿到对象的mutex,吧唧,锁上,(其他子线程拿不到锁,给我看着别动),好的,此时第一个进来的子线程召唤对象的条件变量condition
,等待某个条件,那么等待什么条件呢this->stop || !this->tasks.empty()
,指的是线程池关闭了或者是任务队列不为空,如果条件满足,那就往下走,不满足就直接锁住,直到条件满足,或者其他其他线程调用了条件变量的 nofity 函数来唤醒(注意哦,现在其他几个子线程是被阻塞住的,是不可能来唤醒的,只能让主线程来唤醒),先不考虑线程池关闭,第一次进来当前任务是空的呀,那就等着吧。此时应该可以想到,要让主线程快往任务队列里塞东西呀,子线程好往下走。
假设此时主线程靓仔睡醒了,往任务队列里塞了一个任务,再notify_one
通知刚才那个在等的条件变量,(注意子线程要通过条件变量通知wait住的子线程,否则wait是不会持续不断的检测条件变量是否满足的)假设靓仔马上又睡着了,我们再去看刚才的第一个子线程,锁住的第一个进来的子线程收到通知,发现条件满足了,队列有东西了,继续往下走,取出队列里的第一个任务,然后离开了unique_lock
的作用域,第一个线程开始执行刚拿到的任务,第一个线程开始执行刚拿到的任务,第二个进来的线程取到锁重复上述过程。
也就是说,构造函数里创建了子线程,如果任务队列里一直有任务,子线程就会马上去执行这些任务,不会阻塞。
- 析构在做什么?
需要等子线程把任务都执行完了,主线程再取到锁,修改stop的状态,通知子线程的conditiion 不再wait后释放线程资源。
- enque在做什么?
流程很简单,接收一个functor任务,取到线程池的锁,将functor推入任务队列后通知某个空闲的子线程来取。比较复杂的地方在模板元的部分,在此不赘述