9.1 半同步半异步线程池介绍
传统的一个请求一个线程处理,在处理大量并发任务时,会导致大量的线程创建和销毁,消耗过多的系统资源。线程池可以解决这个问题。
本章介绍的是半同步半异步线程池,线程池结构图如图9-1:
图9-1 半同步半异步线程池
第一层是同步服务层,它处理来自上层的任务请求。上层的请求可能是并发的,这些请求不是马上被处理,而是将这些请求放到一个同步队列里。
第二层是排队层,即同步队列。
第三层是异步服务层,多个线程从同步队列里取出任务并处理。
9.2 线程池实现的关键技术分析
线程池有两个活动过程,一个是向同步队列里添加任务的过程,另一个是从同步队列里取任务的过程,活动图如图9-2:
图9-2 半同步半异步线程池活动图
注意,同步队列会限制任务数的上限,避免任务过多导致内存暴涨。
9.3 同步队列
同步队列的实现代码:
#include <list>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <iostream>
using namespace std;
template<typename T>
class SyncQueue
{
public:
SyncQueue(int maxSize) : m_maxSize(maxSize), m_needStop(false)
{
}
void Put(const T& x)
{
Add(x);
}
void Put(T&& x)
{
Add(std::forward<T>(x));
}
void Take(std::list<T>& list)
{
std::unique_lock<std::mutex> locker(m_mutex);
// return条件都不满足,则wait
m_notEmpty.wait(locker, [this]{return m_needStop || NotEmpty();});
if(m_needStop) return;
// std::move后,m_queue为空!
list = std::move(m_queue);
m_notFull.notify_one();
}
void Take(T& t)
{
std::unique_lock<std::mutex> locker(m_mutex);
m_notEmpty.wait(locker, [this]{return m_needStop || NotEmpty();});
if(m_needStop) return;
t = m_queue.front();
m_queue.pop_front();
m_notFull.notify_one();
}
void Stop()
{
{
std::lock_guard<std::mutex> locker(m_mutex);
m_needStop = true;
}
m_notFull.notify_all();
m_notEmpty.notify_all();
}
bool Empty()
{
std::lock_guard<std::mutex> locker(m_mutex);
return m_queue.empty();
}
bool Full()
{
std::lock_guard<std::mutex> locker(m_mutex);
return m_queue.size() == m_maxSize;
}
size_t Size()
{
std::lock_guard<std::mutex> locker(m_mutex);
return m_queue.size();
}
private:
bool NotFull() const
{
bool full = m_queue.size() >= m_maxSize;
if(full) cout << "缓冲区满了,需要等待..." << endl;
return !full;
}
bool NotEmpty() const
{
bool empty = m_queue.empty();
if(empty) cout << "缓冲区空了,需要等待..." << endl;
return !empty;
}
template<typename F>
void Add(F&& x)
{
std::unique_lock<std::mutex> locker(m_mutex);
m_notFull.wait(locker, [this]{return m_needStop || NotFull();});
if(m_needStop) return;
m_queue.push_back(std::forward<F>x);
m_notEmpty.notify_one();
}
private:
std::list<T> m_queue; //缓冲区
std::mutex m_mutex;
std::condition_variable m_notEmpty; //缓冲区不为空的条件变量
std::condition_variable m_notFull; //缓冲区不满的条件变量
int m_maxSize;
bool m_needStop;
};
Take函数
void Take(std::list<T>& list)
每个数据加锁效率较低,这里改进,一次加锁就能将队列中所有数据取出,大大减少了加锁次数。
m_notEmpy.wait(locker, [this]{ return m_needStop || NotEmpty(); })
判断式由两个条件组成,一个是停止的标志,一个是不为空的条件。当不满足任何一个条件时,条件变量会释放mutex并将线程置于wait状态,等待其他线程notify_one或notify_all将其唤醒。
9.4 线程池
线程池的实现:
#include <list>
#include <thread>
#include <functional>
#include <memory>
#include <atomic>
#include "SyncQueue.hpp"
const int MaxTaskCount = 100;
class ThreadPool
{
public:
using Task = std::function<void(void)>;
ThreadPool(int numThreads = std::thread::hardware_concurrency()) : m_queue(MaxTaskCount)
{
Start(numThreads);
}
~ThreadPool()
{
// 如果没有停止,则主动停止线程池
Stop();
}
void Stop()
{
// 保证多线程下只调用一次StopThreadGroup
std::call_once(m_flag, [this]{ StopThreadGroup(); });
}
void AddTask(Task&& task)
{
m_queue.Put(std::forward<Task>(task));
}
void AddTask(const Task& task)
{
m_queue.Put(task);
}
private:
void Start(int numThreads)
{
m_running = true;
// 创建线程组
for (int i = 0;i < numThreads; i++)
{
// thread runs ThreadPool::RunInThread() on object this
m_threadgroup.push_back(std::make_shared<std::thread>(&ThreadPool::RunInThread, this));
}
}
void RunInThread()
{
while (m_running)
{
std::list<Task> list;
m_queue.Take(list);
for(auto& task:list)
{
if(!m_running) return;
cout << "异步服务层线程id:" << thdId << endl;
task();
}
}
}
void StopThreadGroup()
{
m_queue.Stop();
m_running = false;
for (auto thread : m_threadgroup)
{
if(thread) thread->join();
}
m_threadgroup.clear();
}
// 处理任务的线程组
std::list<std::shared_ptr<std::thread>> m_threadgroup;
// 同步队列
SyncQueue<Task> m_queue;
atomic_bool m_running;
std::once_flag m_flag;
};
9.5 应用实例
void TestPool()
{
ThreadPool pool;
for (int i = 0; i < 10; i++)
{
auto thdId = this_thread::get_id();
pool.AddTask([thdId]{
cout << "同步服务层线程id:" << thdId << endl;
});
}
this_thread::sleep_for(std::chrono::seconds(2));
pool.Stop();
}
线程池会根据当前cpu个数创建对应个数的异步线程。同步服务层线程不断地向同步队列添加任务,线程池里的线程会并行处理。
使用线程池时注意:
(1)要保证线程池中的任务不能挂死,否则会耗尽线程池中的线程。
(2)要避免长时间执行一个任务,会导致后面的任务大量堆积而得不到及时处理。对于耗时较长的任务建议采用单个线程处理。