本篇文章比较杂,除了介绍线程池以及实现之外,还解释一些编程的细则,主要是在对于C++特性使用的过程之中,碰到了诸多的问题,所以就此机会一起解决。实现采用C++
半同步半异步线程池:在处理大量并发任务时,为解决传统大量线程创建和销毁将消耗过多的系统资源的问题,通过建立一个线程池在系统中预先创建一定数量的线程,当任务请求到来时从线程池中分配一个预先创建的线程取处理任务,线程处理任务之后可以重用,等待下次任务到来。这样避免大量创建销毁动作,节约系统资源。(其实说起来很高大上,实现起来则是另一回事)
(图传不上来。。。脑补一下线程池三层结构,同步服务层,排队层,异步服务层)
1、结构划分:
同步服务层:处理来自上层的任务请求,上层的请求可能是并发的,任务放到一个同步队列中,等待处理
排队层:来自上层的任务请求都会加到排队层中等待处理
异步服务层:多个线程同时处理排队层中的任务
2、 实现:其实并不是很复杂,首先需要一个同步队列,这个队列负责存储任务,同时对每个就可能出现互斥访问的地方加锁实现同步操作;然后实现线程池,线程池一般通过shared_ptr指针保存各个线程,同时线程池需要实现线程的方法体(即runthread方法),这个方法体的主要任务就是从同步队列中领任务(执行完了,继续领,直到stop);目前这个半同步半异步线程池已经实现的差不多了,剩下的就是来自同步层的,同步层通过ThreadPool来添加任务,将任务交给ThreadPool中的线程来处理,任务通过同步队列来管理,我更愿意将这一过程说成委托,任务委托给服务线程。
代码:
//synchronous queue
#ifndef _SYNCQUEUE_HPP_
#define _SYNCQUEUE_HPP_
#include<list>
#include<mutex>
#include<thread>
#include<condition_variable>
#include<iostream>
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); //unique_lock自动完成加解锁的工作
m_notEmpty.wait(locker, [this] {return m_needStop || NotEmpty(); });
if (m_needStop)
return;
list = std::move(m_queue); //取整个队列
m_notFull.notify_one();
}
void Take(T& t)
{
std::unique_lock<std::mutex> locker(m_mutex); //unique_lock自动完成加解锁的工作
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;
//性能小优化,被唤醒的线程不需要等待lock_guard释放锁,出了作用域之后,锁就释放了
}
m_notEmpty.notify_all();
m_notFull.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(); //为什么加锁
}
int count()
{
return m_queue.size(); //为什么不加锁
}
private:
bool NotFull() const
{
bool full = m_queue.size() >= m_maxSize;
if (full)
std::cout << "缓冲区已满,需要等待" << std::endl;
return !full;
}
bool NotEmpty() const
{
bool empty = m_queue.empty();
if (empty)
std::cout << "缓冲区已空,需要等待,等待线程ID: " << std::this_thread::get_id() << std::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;
};
#endif // !_SYNCQUEUE_HPP_
//threadpool
#ifndef _THREADPOOL_HPP
#define _THREADPOOL_HPP
#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()>;
ThreadPool(int numThreads = std::thread::hardware_concurrency()) :
m_queue(MaxTaskCount)
{
Start(numThreads);
}
~ThreadPool()
{
Stop();
}
void Stop()
{
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) {
m_threadGroup.push_back(std::make_shared<std::thread>(&ThreadPool::RunThread, this));
//创建numThreads个线程,每个线程主体为Runthread,传入this指针
}
}
void RunThread()
{
while (m_running) {
std::list<Task> list;
m_queue.Take(list);
//m_threadGroup内部线程主体,每个线程拿走一组list开始,执行各个Task任务
for (auto& task : list) {
if (!m_running)
return;
task();
}
}
}
void StopThreadGroup()
{
m_queue.Stop();
m_running = false;
for (auto thread : m_threadGroup) {
if (thread) //基于范围的for遍历
thread->join();
}
m_threadGroup.clear();
}
private:
std::list<std::shared_ptr<std::thread>> m_threadGroup;
SyncQueue<Task> m_queue;
std::atomic_bool m_running; //原子值
std::once_flag m_flag;
};
#endif // !_THREADPOOL_HPP
//main
#include"threadPool.hpp"
void TestThreadPool()
{
ThreadPool pool(3);
//创建一个只有三个线程的异步线程池来为各个线程提供服务
auto f = [&pool](int t) {
for (int i = 0; i < 10; ++i) {
auto thdId = std::this_thread::get_id();
pool.AddTask([thdId, t] {
std::cout << "同步层线程" << t << "的线程ID: " << thdId << std::endl;
});
}
};
//同步层线程,任意个每个线程往队列里面丢任务
std::thread thd1(f, 1);
std::thread thd2(f, 2);
std::thread thd3(f, 3);
std::this_thread::sleep_for(std::chrono::seconds(1));
getchar();
pool.Stop();
thd1.join();
thd2.join();
thd3.join();
}
int main()
{
TestThreadPool();
return 0;
}
3、一些小细节:
①同步队列每一个异步操作的函数都要加锁,比较重要的是在Add,即添加Task的时候使用加锁操作;
②unique_lock和lock_guard的不同,前者随时可以释放锁,而后者只在析构时释放锁;
③更关心的是,多个线程是如何使用这个同步队列的,同步服务是来自外部的请求,通过threadPool委托任务,任务由同步队列来管理
4、关于实现上的一些问题:
①左值与右值引用:左值引用相当于一个变量的别名,通过别名可以直接对其对应的变量进行值操作;而右值引用则是关联到右值的,一点个人的理解,由于右值的生命期很短,比如临时变量,基本在一条语句之内就完成了创建销毁操作。按照定义而言的话,右值不能取地址,没有名字的值;比如一些表达式返回的临时对象,函数返回值(返回的不是引用),之后为了优化程序性能(其中很突出一点是,对象拷贝复制时的性能优化,具体可以参加这篇博客:http://www.cnblogs.com/catch/p/3507883.html 很赞)
而右值引用出现的一个突出的作用是实现移动语义,当发生拷贝的时候,如果知道传来的是一个临时值,以往的做法往往是通过临时值创建临时对象,将这个对象赋值给我们需要的对象,然后依次进行销毁工作;但是如果我们直接将临时值的资源进行一下移动,将它交给我们需要赋值的对象,性能不就得到了提高,这就是移动语义的来处。
②移动语义:实际文件还在原来位置,而只修改记录(理解为:转让资源的所有权)
移动语义的目的在上面已经做过介绍,避免大量无用的构造析构工作。如何实现移动语义,一般而言提供需要编写移动构造函数,这点之后再讲。在此补充一下关于std::move语义,实际上就是强制将一个左值转换成一个右值,至于深度了解可以参照上文的链接。
③完美转发std::forward:在函数模版中,完全依照函数模板的参数类型,保持参数的左值、右值特征,将参数转递给函数模版中调用的另一个函数。
同时介绍一下在参数类型推导上的引用折叠原则:所有的右值引用叠加到右值引用上仍然还是一个右值引用;所有的其它引用类型之间的叠加都将变成左值引用。即T&& && => T&& , T&& & = T&
④一个性能高的类需要哪几种构造函数:主要是在复制构造函数的时候,涉及到对象的深浅拷贝的时候(尤其对象使用的堆内存比较大的情况下),一般而言我们只会提供对象的拷贝构造函数和赋值构造函数;但是拷贝临时对象时,难以避免多次的构造析构;结合上面的,提供右值引用版本的拷贝赋值构造函数,在内部完成移动过语义,优化性能。