一、线程池中的任务
在前面的线程池操作中,任务只是通过std::function来实现。从实际的出发需求来说,基本上一般的线程任务使用其实已经够用。但在实际情况中,可能会遇到一些情况,比如不支持c++11,或者为了某种目的无法使用std::function,那么在这种情况下就需要开发者对任务进行单独的封装。
另外,线程的任务的处理,不是简单的直接对任务暴露给外面即可,更好的方式其实是将任务隐藏起,只对外暴露要使用的相关接口即可。所以在本篇重点是对任务进行一下类似于std::function的封装,同时在外面再增加一层“外覆器”(STL中也有类似的实现)。
二、任务的封装处理
此处把任务的封装分成两块,即任务本身的封装和任务的外覆器封装。而任务本身的封装又分为使用std::function和不使用其进行封装。然后在这个基础,实现整个任务的封装处理。
三、源码
看看源码就明白了,先看一下前面类型擦除的例子:
#include <iostream>
#include <memory>
#include <utility>
#include <queue>//基础应用
typedef void(*pFtask)();
//普通函数
void WorkTask()
{std::cout << "do work,type erase" << std::endl;
}
//仿函数
class WorkTaskFunc
{
public:void operator()()const{std::cout << "functor ,type erase" << std::endl;}
};
//lambda表达式
auto tasklambda = []() {std::cout << "lambda,type erase" << std::endl; };//任务基础抽象类
class BaseTask
{
public:BaseTask() = default;virtual ~BaseTask() = default;
public:virtual void DoWork() const = 0;virtual void operator()()const = 0;
};
//标准任务类
template<typename F>
class TaskImpl :public BaseTask
{
public:TaskImpl() = default;template<typename T>TaskImpl(T&& t) :func_(std::forward<T>(t)) {}~TaskImpl() = default;
public:void DoWork()const override{//WorkTask();std::cout << "start task!" << std::endl;}void operator()()const override{func_();}public:F func_;
};class TaskWrapper;//前向声明
template <typename F>
using is_ok_wrapper =
std::enable_if_t<!std::is_same_v< std::remove_cvref_t<F>, TaskWrapper >,int>;//任务封装打包器
class TaskWrapper
{
public:TaskWrapper() = default;template<typename F, is_ok_wrapper<F> = 0>TaskWrapper(F &&f){using type_decay = std::decay_t<F>;using standType = TaskImpl<type_decay>;//typedef TaskImpl<F> standType;pTask_ = std::make_unique<standType>(std::forward<type_decay>(f));}~TaskWrapper() = default;
public:TaskWrapper(TaskWrapper&& other)noexcept :pTask_(std::move(other.pTask_)) {}TaskWrapper& operator = (TaskWrapper&& rhs)noexcept{pTask_ = std::move(rhs.pTask_);return *this;}TaskWrapper(const TaskWrapper&) = delete;TaskWrapper& operator=(const TaskWrapper&) = delete;
public:void operator()()const{pTask_->operator()();//pTask_->DoWork();}
public:std::unique_ptr<BaseTask> pTask_;
};
其中任务的封装过程在前面的“类型擦除的应用”及“类型擦除应用的优化”中有过分析,如果有什么不太明白的可以去翻回去看看。但是如果需要扩展一下带变参的呢?只需要增加一个变参处理即可。但模板函数是不能做为虚函数的,所以,此处就不需要继承BaskTask了(当然,手写虚表也是可以的,下面的代码中仍然有继承,目的是为了代码的完整性),可以直接在TaskWrapper中增加一个opreator()的重载即可:
#ifndef __TASKIMPL_H__
#define __TASKIMPL_H__#include "BaseTask.h"
#include <functional>
#include <iostream>
template <typename F>
class TaskImpl : public BaseTask {public:TaskImpl() = default;TaskImpl(F &&f) : func_(std::forward<F>(f)) {}~TaskImpl() = default;public:void Run() {}void operator()() const {}void DoWork() const {}template <typename... Args> void operator()(Args... args) const {this->func_(std::forward<Args>(args)...);}private:F func_;
};#endif // __TASKIMPL_H__
class TaskWrapper {
public:TaskWrapper() = default;
......public:template <typename F, typename... Args> void operator()(F &&f, Args... args) const {static_assert(!std::is_same_v<std::remove_cvref_t<F>, TaskImpl<F>>); // c++20using standType = TaskImpl<F>;auto static task = std::make_unique<standType>(std::forward<F>(f));task->operator()(std::forward<Args>(args)...);}private:......
};
当然,可以把“typedef void(*pFtask)()”替换成std::function,在前面的系列中基本都是这样做的。其实大家看很多的开源的线程池,在实际应用的场景下,一般参数都是固定的,所以他们很多参数都是固定的。这样的好处就在于不用和模板来回折腾,至于孰优孰劣,就见仁见智了。
而实际的线程运行,往往是对数据进行处理,这其实更多的是从一个队列中获取,这时参数的传递的意义更小甚至没有,所以开发者一定要根据实际情况来决定你的设计,不要盲目的追求高大上和普适性,这算是一点小的建议吧。
四、总结
线程池中任务的封装处理其实是相当重要的一环,毕竟外来的任务需要从此承载到运行的线程上。也只有这一块设计的灵活可扩展,才能使线程池本身的应用更容易扩展。其实任务的封装有的时候儿可以针对具体的实现来实现,不需要抽象到一定层次。但是一个良好的设计,一定是从顶层抽象良好的。
这本来就是一个矛盾,平衡点就在于开发者对开发的具体的要求和把控。