众所周知 频繁的创建和销毁线程所带来的开销是很大的 一个高性能高并发得服务器都会需要用到线程池技术 那么我们怎么设计一个完美的线程池呢?
1. 我们需要一个线程类,具体的线程函数应该是实际需要的时候 再设置。
2. 不应该限制线程函数的参数数量、作用域(可以是全局函数、 静态成员函数、类成员函数等等)。
3. 我们规定线程函数返回值应该是int,0表示任务结束 1表示需 要再次运行任务 。
4. 线程应该可以控制,在需要的时候启动,并且随时可以终止和 退出。
5. 这个类不应该具有赋值和复制构造方法,因为线程本身不具有 可复制性!
如果读者不同意以上五个观点 那么就没有必要读下去的必要
考虑到第二点,我们必然需要泛型编程技术,或者叫模板编程技术。 基于这些需求我们大概有一个初步的构想。
类名CThread
考虑到很多地方都这么给线程类起名,所以我们可以加入一个名称 空间GodKillerLi。 接口应该有下面这些:
1. 启动/恢复
2. 暂停
3. 停止
4. 重新开始线程
5. 线程是否有效
线程API我们选取Windows下的CreateThread (直接选择系统调用 不要搞些奇奇怪怪的东西浪费效率 如果是linux 则选择pthread_create ) 这个API函数说明如下:(参考自windows官方文档)
#define WINAPI __stdcall
typedef void *HANDLE;
typedef DWORD (WINAPI *PTHREAD_START_ROUTINE)(LPVOID lpThreadParameter
);
typedef PTHREAD_START_ROUTINE
LPTHREAD_START_ROUTINE;
HANDLE
WINAPI
CreateThread(
_In_opt_ LPSECURITY_ATTRIBUTES
lpThreadAttributes,//线程属性
_In_ SIZE_T dwStackSize,//线程栈的大小,为0表示
默认大小
_In_ LPTHREAD_START_ROUTINE lpStartAddress,//
线程入口函数
_In_opt_ __drv_aliasesMem LPVOID
lpParameter,//线程参数
_In_ DWORD dwCreationFlags,//创建标志默认写0,
CREATE_SUSPENDED线程暂停,
STACK_SIZE_PARAM_IS_A_RESERVATION 第二个参数有效(默
认忽略)
_Out_opt_ LPDWORD lpThreadId //线程ID
);
从上面我们知道,线程的API接口需要我们填入一个__stdcall调用约 定的函数,做为线程入口函数。 而且函数参数是固定的,和前面的需求2冲突。 所以我们必然需要一个过度量来记录用户提供的自定义线程函数, 另外需要额外提供一个入口函数,来做为系统API函数的参数。 考虑到线程后面可能用于线程池,所以线程类本身不能是模板类。
注意:模板参数一旦不同,系统会认为这些类是不同的类。所以 用于线程池的时候,会比较麻烦——我们不太可能给每一个子线 程提供控制接口。因为线程池要求所有子线程最好都是同一个类 型,这样扩容或者收缩的时候,就容易实现动态分配和释放。
所以我们还需要一个模板函数类,来接入这些用户动态设置的自定 义线程函数。 由于这个函数类是模板类,所以我们还需要给它设置一个基类,方便线程类来持有它。 避免模板参数的需求传递到线程类来了。
注意:函数类也叫仿函数,一般需要重构函数调用运算符 operator()(...)。
首先我们要把仿函数(函数类)搞出来,否则线程类没法实现。 仿函数代码如下:
namespace GodKillerLi {
class CFunctionBase
{
public:
virtual ~CFunctionBase() = default;
virtual int operator()() = 0;
};
template <class _FX, class... _Types>
class CFunction :public CFunctionBase
{
public:
CFunction(_FX _Func, _Types... _Args)
:m_binder(std::forward<_FX>(_Func),std::forward<_Types>(_Args)...)
{}
virtual ~CFunction() {}
virtual int operator()() {
return m_binder();
}
std::_Binder<int, _FX, _Types...>m_binder;
};
}
仿函数CFunction必须有一个基类CFunctionBase。 这样线程类里面的具体函数可以通过基类指针,来持有函数类。 仿函数最重要的接口就是运算符重载函数。 所以这里我们要虚函数的特性,将函数调用运算符重载函数,设置 为虚函数。 并且要设置为纯虚函数,这样之类如果不实现该接口,则无法实例 化,无法声明对象。 考虑到到时候我们都是通过基类指针操作,为了子类的析构,所以 这里我们要设置虚析构,避免析构的时候出现意外。 基类基本就是一个接口类,所以完全可以不写构造函数。
template 这是一段模板参数声 明代码。 告诉编译系统这样一个事实:下面有一个类型变量FX,和一个可变 参数模板,我们叫它们Types。 至于FX和Types怎么用,需要我们这些开发者来写代码告诉编译系统。
CFunction(_FX _Func, _Types... _Args)
:m_binder(std::forward<_FX>(_Func),std::forward<_Types>(_Args)...)
这段代码则首先指明这样一个事实: 构造函数有一个可变类型的参数_Func,和若干各可变参数类型 _Types... 我们的形式参数Func的类型是FX,_Args则是指代所有可变参数——这绝不是一个参数,但也可能一个参数也没有。 声明清楚这些参数以后,我们很快就需要在m_binder里面使用这些参数。
m_binder是这样一种混合体:包含函数类型和调用参数的混合体。 这种东西很有意思,我们一般写一个函数调用,例如下面这样: func(10,"hello"); 往往执行到这句话的时候,就是func函数被调用的时候。 m_binder则提供了另外一种可能。 写这句话的时候,和执行的时候,被分开了。 在绑定的时候——也就是binder的英文本意,代码并没有被执行。 而什么时候执行,则完全由用户说了算。 唯有执行函数调用运算符的时候,才是真正执行代码的时候。 这种延迟执行的特性,非常重要! 线程函数执行的时候,往往和声明的时候,不是在同一个时间。 有了这个工具,我们就可以准备一个m_binder,去承接用户多样化 的线程函数和函数参数了。 等到需要执行的时候,我们再真正执行这些函数。 这里还有一个小知识,std::forward表示完美转发。
关于左值右值 左值引用 右值引用 万能引用 引用折叠 完美转发等知识点可以阅读如下文章 谈谈C++的左值右值,左右引用,移动语意及完美转发 - 知乎
析构函数里面我们无需做什么事情,因为m_binder会自行进行析 构。 运算符重载我们也无需做什么特别的事情,只需要按照之前的设 置,进行函数调用即可。 这里无需指定参数,因为这些参数已经在构造的时候已经设置好了。
std::_Binder<std::_Unforced, _FX, _Types...> m_binder;
最后就是这个m_binder的声明。我们可以看到_FX,这是函数的类型。 _Types...这个是可变参数的类型。 std::_Unforced是函数的返回类型,意思是可以为void。 也就是无返回类型的函数,也是可以在此设置的。 整个变量就是一个类型不定的函数执行器。 到这里为止,可变的函数类型我们就完成了,下面我们开始线程的实现。
namespace GodKillerLi {
enum CThreadStatus
{
THREAD_AGAIN = 1, //线程再次运行
THREAD_OK = 0, //线程目前一切
正常
THREAD_IS_INVALID = -1, //线程无效
THREAD_PAUSE_ERROR = -2, //线程暂停错误
THREAD_IS_BUSY = -3, //线程占线,被
强制结束
};
class CThread
{
public://std::bind
template< typename _FX, typename...
_Types >
CThread(_FX _Func, _Types... _Args);
CThread();//线程池需要
~CThread();
//禁止复制!!!
CThread(const CThread&) = delete;
CThread& operator=(const CThread&) =delete;
int Start();//开始/恢复
int Pause();//暂停
int Stop();//停止
int Restart();//重启线程(上一次运行必须结束)
inline bool isValid() const;//线程是否有效
template< typename _FX, typename...
_Types >
int SetThreadFunc(_FX _Func, _Types...
_Args);
private:
static DWORD WINAPI ThreadEntry(LPVOID
lpParam);
void EnterThread();//__thiscall
private:
DWORD m_nThreadID;
HANDLE m_hThread;
bool m_bValid;//True表示线程正常 False表示线
程不可用
std::atomic<CFunctionBase*> m_pFunction;
};
}
头文件如上面代码所示。 首先这个类提供两种构造函数
template< typename _FX, typename... _Types >
CThread(_FX _Func, _Types... _Args);
CThread();
一种是带线程函数设定的构造函数,用于单个线程的使用。一种是 无参数的构造函数,用于线程池的初始化。 线程池往往会批量产生若干个线程对象,这个时候无参数的构造函 数,可以提供方便。否则每个线程都需要进行设置。单个线程使用,则没有这种顾虑,所以保留一个带线程函数设置的 构造函数,可以方便单个的时候使用。
注意,构造函数是模板函数,并不意味着线程类是模板函数! 所以这只是表明我们有一个系列的构造函数,用于设置各种不同 需要执行的线程函数。 并不说明线程类本身变成了模板函数。 一个非模板类可以拥有一个或者多个模板函数。 类前面拥有template声明,则该类是模板类,否则则属于非模板 类。 一个类里面包含纯虚函数,则该类是抽象类,否则则属于普通 类。 抽象类、模板类因素相互不干扰,一个类既可以是模板类,也可 以是抽象类,或者两者都不是。
下一个我们要注意的是这段代码:
CThread(const CThread&) = delete;
CThread& operator=(const CThread&) =delete;
这段代码表示线程类的复制构造函数和等于号运算符重载被删除。 一旦有涉及复制构造的行为,编译器会及时报错,避免代码进入后 续的执行环节。 进而避免了线程类对象之间的相互复制和传值。
线程在Windows环境下是无法被复制的(当然,在Linux环境也 无法完美复制。) 线程本身是内核对象,我们通过API去使用这个内核对象。 要复制,需要内核提供这个功能——但是目前微软尚未提供该功 能。 不过我们可以复制线程的句柄。复制线程的句柄又会引发一个新 的问题:谁来释放这个句柄? 如果一个线程对象创建了一个线程,另外一个对象复制了它的句 柄。 另外一个对象析构的时候怎么处理? 如果释放了句柄,原创建者再次释放,就可能导致问题。 创建者没有释放,问题更大——在一个无效的线程句柄上进行操 作,导致的可能就不是问题,而是崩溃了。 最好的方案就是每个线程都是独立,不可复制的,独立管理自己 的线程句柄。
接下来就是我们前面讲的线程接口代码:
int Start();//开始/恢复
int Pause();//暂停
int Stop();//停止
int Restart();//重启线程(上一次运行必须结束)
bool isValid() const;//线程是否有效
//设置线程函数
template< typename _FX, typename... _Types >
int SetThreadFunc(_FX _Func, _Types... _Args);
Start表示开启线程,如果线程处于暂停状态,则线程会恢复运行。 Pause表示暂停线程,注意该操作有风险。一旦暂停,线程所属代 码会暂停执行。这意味着对应的文件读写、网络/管道/信号通信、 异步操作等等,都会发生不可逆转的错误。包括但不限于文件卡 死、网络中断、管道关闭、信号丢失等等。 该功能最佳使用场景是线程池进行规模缩减的时候,将空线程进行 暂停,来缩减线程池规模。这样,线程池需要再次扩张的时候,可以直接通过恢复而不是创建来启动线程。 由于是空闲线程,即使持有者决定彻底析构线程时,也不会有太过 分的负面影响。 Stop表示停止线程。如果线程被用户指定的函数卡死而没有反应, 那么会强制结束线程。所以用户指定的线程函数需要遵循一些规 范,不应该处于卡死状态。 Restart表示重启线程函数。当前一个线程函数执行完成之后,该功 能才能起效。否则该功能只会返回错误值。 isValid表示线程有效状态。返回true表示线程状态正常,否则表示 线程无法执行,处于失效状态。用户指定的线程函数可以通过此接 口来决定自己的状态。一旦线程失效,应该尽快退出函数执行,否 则可能随时被强制结束,而引起更加糟糕的情况出现。 SetThreadFunc设置线程函数。注意如果已经设置过线程函数,此 接口仍然可以调用并设置。设置后,下一次执行线程函数,才会生 效。当前执行的函数不受影响。但是,如果线程没有启动,则设置 后,最新的设置起效,旧有的设置会被丢弃。 对外接口明确下来之后,我们就可以开始考虑内部函数了。
敬请期待下一集咯