C++线程池任务实现(简洁版)

一、前言

 线程池的目标实现线程的复用, 因为线程是操作系统级别的资源, 频繁的创建线程和销毁线程会影响程序的性能。它逻辑的是预先创建一定数量线程, 然后线程池中的线程分别向任务队列取任务来执行, 若当前没有可执行的任务, 则线程池中的线程进入睡眠状态, 避免空耗CPU资源。本文主要记录线程池模型中关于任务的实现。
 关于线程池中的任务实现, 目前我主要见过两种版本:
  ①第一种是利用C++多态性质来实现, 即创建一个 ITask 的虚基类, 然后各个模块根据应用场景去覆写虚基类,通常是基类中声明一个纯虚函数,派生类必须覆写这个函数接口。比如

class ITask
{
public:
	virtual bool Run() = 0;
	
	//other interface
	...
}

class myTask:public ITask
{
public:
	virtual bool Run() override
	{
	 //具体任务的流程
	}
	...
}

任务队列中保存的是基类 ITask 的指针, 线程池中的任务取出任务后调用实际调用派生类的 函数 Run() 执行任务, 通过这种多态封装各种类型的任务。 POCO库中的线程池就是用的这种方式
  ①第二种是利用仿函数,结合C++11中的 Lambda表达式和函数绑定器来实现, github上搜索C++版本的 《theradpool》出来的第一个项目就是用的这种方式, 本文展现的就是这种方式的更多细节。

二、线程池中的任务实现

  C++中可被调用对象分别为以下三种:

① 函数,接受额外传入的参数里列表 作为实参(argument)
② 指向成员函数的指针,当你通过对象调用它,该对象被传递成为第一实参(必须是个reference或pointer),其他实参则一一对应成员函数的参数。
③ 函数对象(function object,该对象拥有operator ()),附带的args被传递作为实参。比如 Lambda 表达式。

前两种方式因为函数的参数有太多不确定性了, 所以通常不能直接用来做线程任务,但是函数对象就方便了, 它有统一的调用方式,并且可以把函数或者指向成员的函数指针包装成 函数对象。所以任务的数据数据类型声明为 std::function<void()>
并且它的相关构造、赋值、移动、拷贝函数如下:

class CXTask
{
public:
   using Function = std::function<void()>;

public:
   inline CXTask();
   inline CXTask(const CXTask& another);
   inline CXTask(CXTask&& another) noexcept;
   inline CXTask& operator=(const CXTask& another);
   inline CXTask& operator=(CXTask&& another);

   inline CXTask(const Function& func);
   inline CXTask(Function&& func);
   inline CXTask& operator=(Function&& func);
   inline CXTask& operator=(const Function& func);

   inline void operator() () const;
   inline operator bool() const;

private:
   Function m_func;
};

 声明为这种方式的任务后线程池中的线程在队列中获得一个任务后 可以直接通过 小括号 task() 执行任务。接下来需要把前文提到的前两种可调用对象转换为 CXTask 类型。具体实现如下:

//任意类型函数和参数
template<class Function, class... Args>
inline CXTask CreateTask(Function&& func, Args&&... args)
{
	return CXTask(std::bind(std::forward<Function>(func), std::forward<Args>(args)...));
}

//函数对象, 比如 lambda
template<class Function>
inline CXTask CreateTask(Function&& func)
{
	return CXTask(std::forward<Function>(func));
}

有了以上的实现后就可以把各种 DIY 的函数放进 CXTask 的任务队列了, 详见测试代码。

三、测试

  测试程序使用C++标准库的 std::queue 来缓存线程任务, 创建两个线程来模拟线程池来执行任务, 使用之前文章中介绍的 (CXEvent) 来进行线程同步。功能上输入数字通过lambda创建任务, 输入字母通过函数绑定器创建对象,按 ‘q’ 退出程序。详细代码如下:

queue<CXTask> g_tasklist;
CXEvent       g_task_notify;
CXEvent       g_quit_notify(CXEvent::Mode::Manual);

void ThreadFunc()
{
	while (!g_quit_notify.isSignal())
	{
		g_task_notify.Wait();
		if (g_tasklist.empty())
		{
			continue;
		}

		auto task = g_tasklist.front();
		if (task)
		{
			task();
		}
		g_tasklist.pop();
	}
}

void TaskFunc(char inputcmd)
{
	cout << "func task -- cur threadID:" << this_thread::get_id() << " --- keyWord is: " << inputcmd << endl;
}

int main()
{
    thread th1(ThreadFunc);
	thread th2(ThreadFunc);
	cout<<"th1 id: "<<th1.get_id()<< " th2 id: "<<th2.get_id() << endl;

	char inputCmd{ 'R' };
	cin.get(inputCmd);
	while ('q' != inputCmd)
	{
		//数字走 lambda创建 task
		if (inputCmd>='0'&& inputCmd <='9')
		{
			g_tasklist.push(CreateTask([inputCmd] {
				cout << "lambda task -- cur threadID:" << this_thread::get_id() << " --- keyWord is: " << inputCmd << endl;
				}));
		}
		
		//字母走 函数创建 task
		if (inputCmd >= 'a' && inputCmd <= 'z')
		{
			g_tasklist.push(CreateTask(TaskFunc, inputCmd));
		}

		g_task_notify.SetEvent();
		
		cin.clear();
		cin.ignore();
		cin.get(inputCmd);
	}
	
	g_quit_notify.SetEvent();

	if (th1.joinable())
	{
		th1.join();
	}

	if (th2.joinable())
	{
		th2.join();
	}
	   
	return 0;
}

测试结果如下:
在这里插入图片描述

四、总结

 本文实现了一种简洁版的线程池任务,实际工作中当然要比这个要复杂很多,比如通过观察者模式引入通知系统, 线程池的调度,任务的优先级管理等等。不过有了这些基础后就能方便看懂一些开源线程池的实现,提高技术。哈哈哈
代码地址:https://github.com/pengguoqing/samples_code/tree/master/c%2B%2B/task

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值