线程池概念
线程池有两个核心的概念,一个是任务队列,一个是工作线程队列。任务队列负责存放主线程需要处理的任务,工作线程队列其实是一个死循环,负责从任务队列中取出和运行任务,可以看成是一个生产者和多个消费者的模型。
为什么要使用线程池?
目前的大多数网络服务器,包括Web服务器、Email服务器以及数据库服务器等都具有一个共同点,就是单位时间内必须处理数目巨大的连接请求,但处理时间却相对较短。
传统多线程方案中我们采用的服务器模型则是一旦接受到请求之后,即创建一个新的线程,由该线程执行任务。任务执行完毕后,线程退出,这就是是“即时创建,即时销毁”的策略。尽管与创建进程相比,创建线程的时间已经大大的缩短,但是如果提交给线程的任务是执行时间较短,而且执行次数极其频繁,那么服务器将处于不停的创建线程,销毁线程的状态。我们将传统方案中的线程执行过程分为三个过程:T1、T2、T3。
-
T1:线程创建时间
-
T2:线程执行时间,包括线程的同步等时间
-
T3:线程销毁时间
那么我们可以看出,线程本身的开销所占的比例为(T1+T3) / (T1+T2+T3)。如果线程执行的时间很短的话,这比开销可能占到20%-50%左右。如果任务执行时间很长的话,这笔开销将是不可忽略的。
除此之外,线程池能够减少创建的线程个数。通常线程池所允许的并发线程是有上界的,如果同时需要并发的线程数超过上界,那么一部分线程将会等待。而传统方案中,如果同时请求数目为2000,那么最坏情况下,系统可能需要产生2000个线程。尽管这不是一个很大的数目,但是也有部分机器可能达不到这种要求。
因此线程池的出现正是着眼于减少线程本身带来的开销。线程池采用预创建的技术,在应用程序启动之后,将立即创建一定数量的线程(N1),放入空闲队列中。这些线程都是处于阻塞(Suspended)状态,不消耗CPU,但占用较小的内存空间。当任务到来后,缓冲池选择一个空闲线程,把任务传入此线程中运行。当N1个线程都在处理任务后,缓冲池自动创建一定数量的新线程,用于处理更多的任务。在任务执行完毕后线程也不退出,而是继续保持在池中等待下一次的任务。当系统比较空闲时,大部分线程都一直处于暂停状态,线程池自动销毁一部分线程,回收系统资源。
基于这种预创建技术,线程池将线程创建和销毁本身所带来的开销分摊到了各个具体的任务上,执行次数越多,每个任务所分担到的线程本身开销则越小,不过我们另外可能需要考虑进去线程之间同步所带来的开销
线程池适合场景
事实上,线程池并不是万能的。它有其特定的使用场合。线程池致力于减少线程本身的开销对应用所产生的影响,这是有前提的,前提就是线程本身开销与线程执行任务相比不可忽略。如果线程本身的开销相对于线程任务执行开销而言是可以忽略不计的,那么此时线程池所带来的好处是不明显的,比如对于FTP服务器以及Telnet服务器,通常传送文件的时间较长,开销较大,那么此时,我们采用线程池未必是理想的方法,我们可以选择“即时创建,即时销毁”的策略。
总之线程池通常适合下面的几个场合:
-
单位时间内处理任务频繁而且任务处理时间短
-
对实时性要求较高。如果接受到任务后在创建线程,可能满足不了实时要求,因此必须采用线程池进行预创建。
-
我们知道线程能共享系统资源,如果同时执行的线程过多,线程并发数量过多,可能导致系统资源不足而产生阻塞的情况。运用线程池能有效的控制线程最大并发数,避免以上的问题。
线程池实现示例一
参考:
https://blog.csdn.net/MOU_IT/article/details/88712090
源代码github:https://github.com/progschj/ThreadPool
#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> //可变参数模板,实现可变形参函数enqueue
auto enqueue(F&& f, Args&&... args) -> std::future<typename std::result_of<F(Args...)>::type>;//任务入队
~ThreadPool(); //析构函数
private:
std::vector< std::thread > workers; //线程队列,每个元素为一个Thread对象
std::queue< std::function<void()> > tasks; //任务队列,每个元素为一个函数对象
std::mutex queue_mutex; //互斥量
std::condition_variable condition; //条件变量
bool stop; //停止
};
// 构造函数,把线程插入线程队列,插入时调用embrace_back(),用匿名函数lambda初始化Thread对象
/*总结:初始化时,线程中的每个函数都会阻塞到条件变量那里,
当任务队列中新加入一个任务时,通知阻塞到条件变量的某一个线程,
接着这个线程执行:互斥量加锁——>任务队列出队——>互斥量解锁——>执行任务。
当线程执行完任务之后,如果任务队列不为空,则继续从任务队列那里取出任务执行,
如果任务队列为空则阻塞到条件变量那里。
*/
inline ThreadPool::ThreadPool(size_t threads) : stop(false){
for(size_t i = 0; i<threads; ++i)
workers.emplace_back(
[this]
{// 线程内不断的从任务队列取任务执行
for(;;)
{
// task是一个函数类型,从任务队列接收任务
std::function<void()> task;
//用{}添加了一个作用域。
{
//给互斥量加锁,锁对象生命周期结束后自动解锁
std::unique_lock<std::mutex> lock(this->queue_mutex);
//(1)当匿名函数返回false时才阻塞线程,阻塞时自动释放锁。
//(2)当匿名函数返回true且受到通知时解阻塞,然后加锁。
//这里是当线程池运行(stop==false)并且任务列表为空时,阻塞线程
this->condition.wait(lock,[this]{ return this->stop || !this->tasks.empty(); });
// 执行条件变量等待的时候,已经拿到了锁(即lock已经拿到锁)
// 这里将会unlock释放锁,其他线程可以继续拿锁,但此处任然阻塞,等待条件成立
// 一旦收到其他线程notify_*唤醒,则再次lock,然后进行条件判断
// 当[return this->stop || !this->tasks.empty()]的结果为false将阻塞
// 条件为true时候解除阻塞。此时lock依然为锁住状态
//线程池停止(stop==true)且任务列表为空时,线程退出结束
if(this->stop && this->tasks.empty())
return;
//从任务队列取出一个任务
task = std::move(this->tasks.front());
this->tasks.pop();
}// 离开{}作用域,自动解锁
task(); // 执行这个任务
}
}
);
}
// 添加新的任务到任务队列
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)...)
);
//获取任务的futrue
std::future<return_type> res = task->get_future();
{
std::unique_lock<std::mutex> lock(queue_mutex); //加锁
if(stop)
throw std::runtime_error("enqueue on stopped ThreadPool");
tasks.emplace([task](){ (*task)(); }); //把任务加入队列
} //自动解锁
condition.notify_one(); //通知条件变量,唤醒一个线程
return res;
}
// 析构函数,删除所有线程
inline ThreadPool::~ThreadPool()
{
{
std::unique_lock<std::mutex> lock(queue_mutex);
stop = true;
}
// 通知所有工作线程,
condition.notify_all();
// 等待所有工作线程结束,这里会执行完任务队列中剩余的所有任务后才结束
for(std::thread &worker: workers)
worker.join();
}
#endif
备注:
来自https://blog.csdn.net/gavingreenson/article/details/72770818>
1 < thread>: 是 C++ 11的新特性,主要包含了线程对象std::thread的构造。
2 < mutex>: C++ 11新特性,主要包含各种Mutex的类的构造,主要是std::mutex。
3 < condition_variable>: C++ 11新特性, 包含多线程中常用的条件变量的声明,例如notify_one、wait、wait_for等等。
4 < future>: C++ 11新特性,可以获取异步任务的结果,可用来实现同步。包括std::sync和std::future。
5 < functional>: C++ 11增加了一些新特性,简单来说可以实现函数到对象的绑定,如bind()函数。
1 template < class F, class… Args> auto enqueue(F&& f, Args&&… args) -> std::future< typename std::result_of< F(Args…)>::type>;
理解了这一句,这个程序就差不多弄懂了。
首先,这是一个函数模板,而不是类模板。
template<> 部分: template < class F, class… Args>。class… Args代表接受多个参数。
返回类型: auto
函数名: enqueue
形参表: (F&& f, Args&&… args)。&&是C++ 11新特性,代表右值引用。
不明觉厉: -> std::future< typename std::result_of< F(Args…)>::type>。这个->符号其实用到了C++ 11中的lamda表达式,后面的内容代表函数的返回类型。
总的来说就是,这句话声明了一个名为enqueue()的函数模板,它的模板类型为class F以及多个其他类型Args,它的形参是一个F&&类型的f以及多个Args&&类型的args,最后这个函数返回类型是std::future< typename std::result_of < F(Args…)>::type >对于这个冗长的返回类型,又可以继续分析:
std::future在前面提到过了,它本身是一个模板,包含在 < future>中。通过std::future可以返回这个A类型的异步任务的结果。
std::result_of::type就是这段代码中的A类型。result_of获取了someTask的执行结果的类型。
F(Args…)_就是这段代码的someTask,即函数 F(Args…)。
所以最后这个模板函数enqueue()的返回值类型就是F(Args…)的异步执行结果类型。
2 std::vector < std::thread> workers: 像注释说的那样,用来保存线程对象
3 std::queue < std::function\void()>> tasks: 任务队列
4 queue_mutex和condition: 线程同步需要的变量
1 inline: 类似宏定义,会建议编译器把函数以直接展开的形式放入目标代码而不是以入栈调用的形式。通常函数体内代码比较长或者体内出现循环时不宜使用内联,这样会造成代码膨胀。具体参考《Effective C++》: 第30条 。
2 workers.emplace_back([this]{…});
emplace_back()与push_back()类似,但是前者更适合用来传递对象,因为它可以避免对象作为参数被传递时在拷贝成员上的开销。
这里emplace_back()了一个lambda表达式[this]{…}。lambda表达式本身代表一个匿名函数(即没有函数名的函数),
通常格式为[ 捕获列表 ](参数列表)-> 返回类型{函数体}。而在本代码中的lambda表达式是作为一个线程放入workers[]中。.这个线程是个for(;;)循环。
3 for(;;)里面: 每次循环首先声明一个std::function< void()> task,task是一个可以被封装成对象的函数,在此作为最小任务单位。然后用{}添加了一个作用域。
4 作用域里面: 在这个作用域中进行了一些线程上锁和线程状态的判断。
5 lock(this->queue_mutex): 声明上锁原语
6 condition.wait(lock, [this]{…}): 使当前线程进入阻塞状态: 当第二个参数为false时,wait()会阻塞当前线程,为true时解除阻塞;在本例中的条件就是,当线程池运行且任务列表为空时,线程进入阻塞态。
然后判断,线程池停止(stop==true)且任务列表为空时,线程退出结束,否则向下运行。
std::move()是移动构造函数,相当于效率更高的拷贝构造函数。最后将tasks[]任务队列的第一个任务出栈。
7 离开作用域: 然后执行task(),当前一轮循环结束。
1 using … = typename …; 功能类似typedef。将return_type声明为一个result_of< F(Args…)>::type类型,即函数F(Args…)的返回值类型。
2 make_shared < packaged_task < >>(bind()): 又是复杂的嵌套。
make_shared : 开辟()个类型为<>的内存
packaged_task : 把任务打包,这里打包的是return_type
bind : 绑定函数f, 参数为args…
forward : 使()转化为<>相同类型的左值或右值引用
简单来说,这句话相当于把函数f和它的参数args…打包为一个模板内定义的task,便于后续操作。
3 res = task->get_future(): 与模板函数的返回类型一致,是函数异步的执行结果。
4 新作用域: 先是一个加锁原语lock()。
然后是个异常处理,如果停止的话抛出一个运行时异常。
最后,向任务列表插入这个任务task{(*task)();}。
5 condition.notify_one(): 解除一个正在等待唤醒的线程的阻塞态。
6 返回异步结果res
使用示例
#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;
system("pause");
return 0;
}
- 一个问题:类的成员函数怎么用上这个线程池?
待补充。
线程池实现示例二
githubhttps://github.com/log4cplus/ThreadPool/blob/master/ThreadPool.h
#ifndef THREAD_POOL_H_7ea1ee6b_4f17_4c09_b76b_3d44e102400c
#define THREAD_POOL_H_7ea1ee6b_4f17_4c09_b76b_3d44e102400c
#include <vector>
#include <queue>
#include <memory>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <future>
#include <atomic>
#include <functional>
#include <stdexcept>
#include <algorithm>
#include <cassert>
namespace progschj {
class ThreadPool {
public:
explicit ThreadPool(std::size_t threads
= (std::max)(2u, std::thread::hardware_concurrency()));
template<class F, class... Args>
auto enqueue(F&& f, Args&&... args)
-> std::future<typename std::result_of<F(Args...)>::type>;
void wait_until_empty();
void wait_until_nothing_in_flight();
void set_queue_size_limit(std::size_t limit);
void set_pool_size(std::size_t limit);
~ThreadPool();
private:
void start_worker(std::size_t worker_number,
std::unique_lock<std::mutex> const &lock);
// need to keep track of threads so we can join them
std::vector< std::thread > workers;
// target pool size
std::size_t pool_size;
// the task queue
std::queue< std::function<void()> > tasks;
// queue length limit
std::size_t max_queue_size = 100000;
// stop signal
bool stop = false;
// synchronization
std::mutex queue_mutex;
std::condition_variable condition_producers;
std::condition_variable condition_consumers;
std::mutex in_flight_mutex;
std::condition_variable in_flight_condition;
std::atomic<std::size_t> in_flight;
struct handle_in_flight_decrement
{
ThreadPool & tp;
handle_in_flight_decrement(ThreadPool & tp_)
: tp(tp_)
{ }
~handle_in_flight_decrement()
{
std::size_t prev
= std::atomic_fetch_sub_explicit(&tp.in_flight,
std::size_t(1),
std::memory_order_acq_rel);
if (prev == 1)
{
std::unique_lock<std::mutex> guard(tp.in_flight_mutex);
tp.in_flight_condition.notify_all();
}
}
};
};
// the constructor just launches some amount of workers
inline ThreadPool::ThreadPool(std::size_t threads)
: pool_size(threads)
, in_flight(0)
{
std::unique_lock<std::mutex> lock(this->queue_mutex);
for (std::size_t i = 0; i != threads; ++i)
start_worker(i, lock);
}
// 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);
if (tasks.size () >= max_queue_size)
// wait for the queue to empty or be stopped
condition_producers.wait(lock,
[this]
{
return tasks.size () < max_queue_size
|| stop;
});
// don't allow enqueueing after stopping the pool
if (stop)
throw std::runtime_error("enqueue on stopped ThreadPool");
tasks.emplace([task](){ (*task)(); });
std::atomic_fetch_add_explicit(&in_flight,
std::size_t(1),
std::memory_order_relaxed);
condition_consumers.notify_one();
return res;
}
// the destructor joins all threads
inline ThreadPool::~ThreadPool()
{
std::unique_lock<std::mutex> lock(queue_mutex);
stop = true;
pool_size = 0;
condition_consumers.notify_all();
condition_producers.notify_all();
condition_consumers.wait(lock, [this]{ return this->workers.empty(); });
assert(in_flight == 0);
}
inline void ThreadPool::wait_until_empty()
{
std::unique_lock<std::mutex> lock(this->queue_mutex);
this->condition_producers.wait(lock,
[this]{ return this->tasks.empty(); });
}
inline void ThreadPool::wait_until_nothing_in_flight()
{
std::unique_lock<std::mutex> lock(this->in_flight_mutex);
this->in_flight_condition.wait(lock,
[this]{ return this->in_flight == 0; });
}
inline void ThreadPool::set_queue_size_limit(std::size_t limit)
{
std::unique_lock<std::mutex> lock(this->queue_mutex);
if (stop)
return;
std::size_t const old_limit = max_queue_size;
max_queue_size = (std::max)(limit, std::size_t(1));
if (old_limit < max_queue_size)
condition_producers.notify_all();
}
inline void ThreadPool::set_pool_size(std::size_t limit)
{
if (limit < 1)
limit = 1;
std::unique_lock<std::mutex> lock(this->queue_mutex);
if (stop)
return;
std::size_t const old_size = pool_size;
assert(this->workers.size() >= old_size);
pool_size = limit;
if (pool_size > old_size)
{
// create new worker threads
// it is possible that some of these are still running because
// they have not stopped yet after a pool size reduction, such
// workers will just keep running
for (std::size_t i = old_size; i != pool_size; ++i)
start_worker(i, lock);
}
else if (pool_size < old_size)
// notify all worker threads to start downsizing
this->condition_consumers.notify_all();
}
inline void ThreadPool::start_worker(
std::size_t worker_number, std::unique_lock<std::mutex> const &lock)
{
assert(lock.owns_lock() && lock.mutex() == &this->queue_mutex);
assert(worker_number <= this->workers.size());
auto worker_func =
[this, worker_number]
{
for(;;)
{
std::function<void()> task;
bool notify;
{
std::unique_lock<std::mutex> lock(this->queue_mutex);
this->condition_consumers.wait(lock,
[this, worker_number]{
return this->stop || !this->tasks.empty()
|| pool_size < worker_number + 1; });
// deal with downsizing of thread pool or shutdown
if ((this->stop && this->tasks.empty())
|| (!this->stop && pool_size < worker_number + 1))
{
// detach this worker, effectively marking it stopped
this->workers[worker_number].detach();
// downsize the workers vector as much as possible
while (this->workers.size() > pool_size
&& !this->workers.back().joinable())
this->workers.pop_back();
// if this is was last worker, notify the destructor
if (this->workers.empty())
this->condition_consumers.notify_all();
return;
}
else if (!this->tasks.empty())
{
task = std::move(this->tasks.front());
this->tasks.pop();
notify = this->tasks.size() + 1 == max_queue_size
|| this->tasks.empty();
}
else
continue;
}
handle_in_flight_decrement guard(*this);
if (notify)
{
std::unique_lock<std::mutex> lock(this->queue_mutex);
condition_producers.notify_all();
}
task();
}
};
if (worker_number < this->workers.size()) {
std::thread & worker = this->workers[worker_number];
// start only if not already running
if (!worker.joinable()) {
worker = std::thread(worker_func);
}
} else
this->workers.push_back(std::thread(worker_func));
}
} // namespace progschj
#endif // THREAD_POOL_H_7ea1ee6b_4f17_4c09_b76b_3d44e102400c
Basic usage:
// 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;