一、线程池中的任务
在前面的线程池操作中,任务只是通过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++20
using 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,在前面的系列中基本都是这样做的。其实大家看很多的开源的线程池,在实际应用的场景下,一般参数都是固定的,所以他们很多参数都是固定的。这样的好处就在于不用和模板来回折腾,至于孰优孰劣,就见仁见智了。
而实际的线程运行,往往是对数据进行处理,这其实更多的是从一个队列中获取,这时参数的传递的意义更小甚至没有,所以开发者一定要根据实际情况来决定你的设计,不要盲目的追求高大上和普适性,这算是一点小的建议吧。
四、总结
线程池中任务的封装处理其实是相当重要的一环,毕竟外来的任务需要从此承载到运行的线程上。也只有这一块设计的灵活可扩展,才能使线程池本身的应用更容易扩展。其实任务的封装有的时候儿可以针对具体的实现来实现,不需要抽象到一定层次。但是一个良好的设计,一定是从顶层抽象良好的。
这本来就是一个矛盾,平衡点就在于开发者对开发的具体的要求和把控。