手把手教你实现一个完美的线程池(可商用)第一集 c++高阶开发_杀神李的博客-CSDN博客
上文已经把整个线程类和仿函数搞的差不多了 现在我们来看具体实现
首先,由于系统API的要求,我们必须提供一个线程入口函数。
static DWORD WINAPI ThreadEntry(LPVOID lpParam);
这个函数必须是一个 __stdcall 调用约定的函数,所以只能是静态 函数。 因为任何类的非静态成员函数,都是__thiscall 调用约定的函数。 所以这里只能是静态成员函数,以去除__thiscall 调用约定的影响。 当然,我们提供的真正的线程函数是下面这个函数:
void EnterThread();//__thiscall
这两个函数是如何跳转的,我们可以看看它们的实现代码: 首先通过线程创建API创建线程:
m_hThread = CreateThread(
NULL, 0, CThread::ThreadEntry,
this, CREATE_SUSPENDED, &m_nThreadID);
线程入口函数设置为CThread::ThreadEntry这个静态函数。 线程的参数传入了this,也就是线程类自己的地址。 线程的初始状态是暂停状态,后面通过Start启动。 通过这种方式,Start的逻辑被统一了,任何时候都是恢复线程即 可。 无需区别是暂停后恢复的,还是初始化后恢复的了。 接下来就是入口函数了:
DWORD WINAPI GodKillerLi::CThread::ThreadEntry(LPVOID
lpParam)
{
CThread* thiz = (CThread*)lpParam;
thiz->EnterThread();
ExitThread(THREAD_OK);
return THREAD_OK;
}
这里的lpParam依据前面的逻辑,必然是上面我们标记出来的this参 数。 所以我们只需要把这个参数还原成CThread指针,即可调用对应的 EnterThread函数了。 这样一过渡,我们就成功的把静态成员函数转到成员函数。使得线 程的各种属性都可以发挥作用了。 线程函数EnterThread执行完成之后,我们需要执行ExitThread(THREAD_OK);来真正结束线程。 注意,后面的return THREAD_OK;其实并不会被执行到。 真正的线程主体函数如下:
void GodKillerLi::CThread::EnterThread()
{
int ret = 0;
m_bValid = true;
do {
while (m_pFunction == NULL) {
if (m_bValid == false)return;
Sleep(1);
}
CFunctionBase* func = m_pFunction;
if (func == NULL)break;
ret = (*func)();
} while (ret == THREAD_AGAIN);
m_bValid = false;
}
这个函数首先会等待自定义线程函数的设置。
while (m_pFunction == NULL) {
if (m_bValid == false)return;
Sleep(1);
}
这一步主要是应对需要多次运行的线程函数而写的。 确定线程函数无恙之后,我们就会取出自定义线程函数的值:
CFunctionBase* func = m_pFunction;
取出之后,需要再次验证一下函数是否存在:
if (func == NULL)break;
这里是防止多线程环境下,其他线程通过线程对象接口,重置了自 定义线程函数,从而使得func为空。 这样取出成员变量到线程函数的本地变量之后,外面再怎么更改这 个成员属性,都不会影响到本次函数的运行了。 然后我们通过函数调用运算符,调用用户设置的函数,完成对线程 函数的调用:
ret = (*func)();
注意,返回值ret会影响后续的函数走向。 如果是返回THREAD_AGAIN,那么线程函数可能会被再次执行。 其他值,则终止线程函数,结束线程运行。
了解完核心逻辑,我们再来看看线程类的属性:
DWORD m_nThreadID;
HANDLE m_hThread;
bool m_bValid;//True表示线程正常 False表示线
程不可用
std::atomic<CFunctionBase*> m_pFunction;
m_nThreadID 记录的是本线程的ID
m_hThread 记录的是本线程的句柄
m_bValid 记录的是本线程的状态,true表示线程正常(包括暂停 状态),false表示线程异常(未初始化、线程执行完成退出等)
m_pFunction 记录的是用户自定义函数,初始值为NULL。可以通 过构造函数和SetThreadFunc 来进行设置。 m_pFunction是原子类的对象。这样可以避免多线程环境下 SetThreadFunc 突然设置该属性,导致不可知的问题。 现在关于线程类的详细定义我们已经说完了。
具体实现如下
template< typename _FX, typename... _Types >
GodKillerLi::CThread::CThread(_FX _Func, _Types...
_Args)
:m_pFunction(new CFunction<_FX, _Types...>
(_Func, _Args...))
{
m_bValid = false;
m_hThread = CreateThread(
NULL, 0, CThread::ThreadEntry,
this, CREATE_SUSPENDED, &m_nThreadID);
}
GodKillerLi::CThread::CThread() :m_pFunction(NULL)
{
m_bValid = false;
m_hThread = CreateThread(
NULL, 0, CThread::ThreadEntry,
this, CREATE_SUSPENDED, &m_nThreadID);
}
GodKillerLi::CThread::~CThread()
{
Stop();
CFunctionBase* func = m_pFunction;
m_pFunction = NULL;
delete func;
}
template<typename _FX, typename ..._Types>
int GodKillerLi::CThread::SetThreadFunc(_FX _Func,
_Types ..._Args)
{
if (_Func == NULL) {
m_pFunction = NULL;
}
else {
m_pFunction = new CFunction<_FX,
_Types...>(_Func, _Args...);
}
return 0;
}
int GodKillerLi::CThread::Start()
{
if (m_pFunction == NULL) {
return THREAD_IS_INVALID;
}
if ((m_hThread != NULL) && (m_bValid ==
false)) {
DWORD ret = ResumeThread(m_hThread);
if (ret == -1) {
return ret;
}
}
return THREAD_OK;
}
int GodKillerLi::CThread::Pause()
{
if ((m_hThread != NULL) && (m_bValid ==
true)) {
DWORD ret = SuspendThread(m_hThread);
if (ret == -1) {
return THREAD_PAUSE_ERROR;
}
}
else {//线程无效
return THREAD_IS_INVALID;
}
return THREAD_OK;
}
int GodKillerLi::CThread::Stop()
{
m_bValid = false;//设计标志
if (m_hThread != NULL) {
//析构的时候没有结束,则强制结束线程
DWORD ret =
WaitForSingleObject(m_hThread, 10);
if (ret == WAIT_TIMEOUT) {
//尽量不要走到这一步来,否则在线程中new出
来的内存将失控!!!
TerminateThread(m_hThread,
THREAD_IS_BUSY);
}
CloseHandle(m_hThread);
m_hThread = NULL;
}
return THREAD_OK;
}
int GodKillerLi::CThread::Restart()
{
if (m_pFunction == NULL) {//自定义函数有效
return THREAD_IS_INVALID;
}
if (m_bValid != false || (m_hThread !=
NULL)) {
//线程仍然在运行
return THREAD_IS_BUSY;
}
m_hThread = CreateThread(
NULL, 0, CThread::ThreadEntry,
this, 0, &m_nThreadID);
return THREAD_OK;
}
bool GodKillerLi::CThread::isValid() const
{
return m_bValid && (m_pFunction != NULL);
}
部分已经展示的代码我已经去除了,大家可以看前面关于入口函数 和线程函数的定义。 这些代码的作用我就不赘述了,我只讲讲一些注意要点: 首先是关于构造函数。 我们可以看到,无论是带参数的构造函数还不带参数的构造函数, 我们都会把线程给创建出来。 一方面,线程真正的入口函数在线程自己内部,外部不可见。所以 我们想创建就可以尽早的创建。 另一方面,线程的创建消耗的资源要远高于其他对象的创建。所以 早点创建,等到后面Start的时候,就不会把所有消耗集中在一个点 上。这一方面有时候会导致程序对资源的需求呈现冲击波式的需 求,让系统的稳定性比较糟糕。 其次,在任何操作之前,一定要检查一下,操作所依赖的成员属性 的有效性。 因为我们是在多线程环境下进行编程,很多时候一些属性的变化是 发生在其他线程。本线程的程序逻辑是看不见变化的。 我们可以看到开始、暂停的时候,我们做了大量检测,尽量保证后 续操作的有效性。
接着我们来看看停止操作:
int GodKillerLi::CThread::Stop()
{
m_bValid = false;//设计标志
if (m_hThread != NULL) {
//析构的时候没有结束,则强制结束线程
DWORD ret =
WaitForSingleObject(m_hThread, 10);
if (ret == WAIT_TIMEOUT) {
//尽量不要走到这一步来,否则在线程中new出来
的内存将失控!!!
TerminateThread(m_hThread,
THREAD_IS_BUSY);
}CloseHandle(m_hThread);
m_hThread = NULL;
}
return THREAD_OK;
}
停止操作之前,我们需要先把线程的状态设置为失效状态。 这样可以防止在停止期间,其他线程去操作Start、Pause等接口, 来干扰停止动作。 然后判断一下线程的状态。 如果线程已经不存在(可能是没有初始化,也可能是其他线程已经 调用了Stop接口),则直接返回成功。 另外,如果线程只要存在,我们就一定会结束线程。 当然,直接调用TerminateThread也不太合适。 所以我们给了10ms秒的时间给线程去结束自己。 如果线程没有能够结束自己,说明线程已经失控,处于卡死状态。 这个时候,只能强制结束线程。 至于给出的缓冲时间,并非固定的10ms。 这可以是一个动态调整的值,依据实际项目的情况而定。 时间长度可以是1ms到无穷大,都可以。 另外WaitForSingleObject可以用来检测线程句柄的状态。 如果返回WAIT_ABANDONED,则表示线程已经终止。 如果返回WAIT_OBJECT_0,则表示线程正常结束。 如果返回WAIT_TIMEOUT,则表示线程仍然在运行。 如果返回-1,则表示参数有问题。比如第一个参数值根本不是一个 句柄。 最后,Restart 函数描述了如何重启线程。
重启线程需要注意的几点:
1. 前一个线程函数仍然在运行的时候不能重启线程。这是因为线程 类只有一个线程句柄,如果启动多个线程,线程的结束会存在失 控的风险。比如这里,如果我们强制启动一个线程,哪怕前一个 线程还在执行。那么线程的句柄和ID就会更新,而前一个线程的 CloseHandle(m_hThread); m_hThread = NULL; } return THREAD_OK; } 11 12 13 14 15 句柄和ID会丢失。万一这个时候前一个线程无法结束,整个进程 结束的时候,可能会因为这个线程无法结束而卡死。
2. 重启之前应该判断一下自定义函数是否有效。如果自定义函数都 没有有效设置,那么重启也是无意义的。这个时候应该阻止这样 的行为出现。虽然自定义函数无效,不会导致线程函数本身崩 溃,但是对线程的执行逻辑来讲,是一种损害。
3. 重启的时候,线程状态不应该以暂停状态做为初始化状态。而是 应该直接就运行起来。 至此,我们现在完成了线程类的设计与实现。
敬请期待下集