半同步半异步线程池

     本篇文章比较杂,除了介绍线程池以及实现之外,还解释一些编程的细则,主要是在对于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&

 

④一个性能高的类需要哪几种构造函数:主要是在复制构造函数的时候,涉及到对象的深浅拷贝的时候(尤其对象使用的堆内存比较大的情况下),一般而言我们只会提供对象的拷贝构造函数和赋值构造函数;但是拷贝临时对象时,难以避免多次的构造析构;结合上面的,提供右值引用版本的拷贝赋值构造函数,在内部完成移动过语义,优化性能。

 

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值