讲起编程理论天花乱坠,现实却跟山顶洞人一样。
目录
一、问题
前两年维护一些老程序,发现这么用线程的:
HANDLE hThread=CreateThread(。。。。。。);
CloseHandle(hThread);
就是这么潇洒!
是不是线程里面有高级机制控制啊?我啃了源代码啊,没有,真的没有。
为啥啃源代码啊,随机BUG了呗。虽然不确定BUG怎么引起的,先解决不受控的线程总是没错的。
二、跟踪线程创建
所以要记录有哪些线程、检测线程状态,为此写了一个这么一个类:
//线程管理,目前主要用来在结束时先关闭子线程
class CThreadManager
{
private:
struct struct_thread_info
{
HANDLE hThread;
string file;//创建此线程的代码位置
int line;//创建此线程的代码位置
};
vector<struct_thread_info > m_managered_threads;
void ClearFinishedThread(bool stop);
public:
void AddThreadHandle(HANDLE h, char const* file, int line)
{
ClearFinishedThread(false);
struct_thread_info tmp;
tmp.hThread = h;
tmp.file = file;
tmp.line = line;
m_managered_threads.push_back(tmp);
}
void StopAllThread();
};
extern CThreadManager g_CThreadManager;
#define AddManageredThread(h) g_CThreadManager.AddThreadHandle((h),__FILE__,__LINE__)
结构struct_thread_info记录线程句柄和创建位置,方便找到相关代码。
宏AddManageredThread(h)简化了调用方法。
全局对象和两个成员函数:
CThreadManager g_CThreadManager;
void CThreadManager::ClearFinishedThread(bool stop)
{
for (int i = m_managered_threads.size() - 1; i >= 0; --i)
{
DWORD exitcode;
if (!GetExitCodeThread(m_managered_threads[i].hThread, &exitcode))
{
MessageBox(NULL, "GetExitCodeThread失败", "出错", 0);
return;
}
if (STILL_ACTIVE != exitcode)
{
CloseHandle(m_managered_threads[i].hThread);
m_managered_threads.erase(m_managered_threads.begin() + i);
}
else
{
if (stop)
{
TerminateThread(m_managered_threads[i].hThread, 1);
CloseHandle(m_managered_threads[i].hThread);
m_managered_threads.erase(m_managered_threads.begin() + i);
}
}
}
}
void CThreadManager::StopAllThread()
{
ClearFinishedThread(true);
}
调用的时候用AddManageredThread代替CloseHandle就可以了,程序退出的时候先执行StopAllThread结束所有子线程,免得因为退出的先后顺序出异常。
AddManageredThread的特点是自动回收已经结束的子线程(所以在程序结束前最多存在一个已经结束而未回收的线程)。GetExitCodeThread获取子线程结束状态。
上面的代码在结束时直接强制终止了子线程,安全的方法是设置全局变量,所有子线程不时检测全局变量状态,实现受控的退出。对于控制线程退出而言,全局变量已经足够好了,其它机制,比如信号、消息,只会带来不必要的复杂度而不会减轻任何编码量。
CloseHandle的特点是让子线程结束后自动回收,但是不知道子线程什么时候结束嘛,程序结束前难免手动清理一些资源,而子线程却在使用这些资源,这不就BUG了嘛。
三、多线程的一般原则
多线程程序最基本的原则就是子线程应该是受管理的,最起码的,就像上面写的类一样,要把创建和停止管理起来,在程序退出的时候先关闭所有子线程,再退出主程序。
线程的交互尽量简单,尽量用一个独立的交互区来操作,不要让每个线程都可以随意使用整个程序的数据,数据保护很难做。
线程里面不要再创建线程,很难管理。
从基本完成一个程序到解决随机BUG很漫长。学会使用CreateThread不算什么,功夫都在自己的代码逻辑里。
四、相关技术点
4.1 CreateThread
HANDLE CreateThread(
[in, optional] LPSECURITY_ATTRIBUTES lpThreadAttributes,
[in] SIZE_T dwStackSize,
[in] LPTHREAD_START_ROUTINE lpStartAddress,
[in, optional] __drv_aliasesMem LPVOID lpParameter,
[in] DWORD dwCreationFlags,
[out, optional] LPDWORD lpThreadId
);
创建线程,参数很多不过大部分都可以不用。
4.2 CloseHandle
BOOL CloseHandle(
[in] HANDLE hObject
);
关闭句柄。这个API的意思并不是删除句柄代表的资源,只是表示结束对资源的使用。对于线程而言,这个操作并不表示结束线程,而是表示让它运行完了自己结束就好了。
线程的访问计数和别的资源不一样,线程一启动计数就是2,CloseHandle只会减少1,所以CloseHandle之后线程还有计数1,仍是有效的。
4.3 GetExitCodeThread
BOOL GetExitCodeThread(
[in] HANDLE hThread,
[out] LPDWORD lpExitCode
);
获取线程退出状态。如果线程没有退出,要自己想办法,直接杀死线程太粗暴了。
4.4 TerminateThread
BOOL TerminateThread(
[in, out] HANDLE hThread,
[in] DWORD dwExitCode
);
杀死线程,很不好。dwExitCode设置线程返回值——你自己杀死的你不知道咋回事吗?这个返回值是给调用GetExitCodeThread的人看的,不是给自己看的。
以上代码是以win7+vs2010为目标的,可在win10、win11上运行。
(这里是文档结束)