浅谈Windows下的线程细节
序言
最近阅览了《windows中心编程》关于线程的章节,原书作者评论得较为深化,初读者极易被绕晕,我专门写这篇文章供初读者参阅阅览。本文的最终,侧重评论了Windows线程API与c/c++运转时库的留意事项。由于自己水平有限,文章不免有疏忽,还望各位读者纠正。
Windows供给的创捷与销完毕程的函数
咱们知道,Windows供给了CreateThread函数用于创立线程,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
);
其间各参数的含义在此不多赘述。接下来咱们侧重评论一下lpStartAddress和lpParameter参数。
lpStartAddress 和 lpParameter参数
lpStartAddress
指向要由线程履行的应用程序界说函数的指针。 此指针表明线程的开端地址。
lpParameter
指向要传递给线程的变量的指针。
从字面意思上了解这两个参数是不难的,问题在于Windows是一个抢占式的多线程体系,也便是说,调用CreateThread函数的线程与CreateThread函数创立的线程是一起履行的,这会引发一些难以捕捉的反常。
考虑如下C++代码:
static void WINAPI Print(int* x)
{
_tprintf(TEXT("Print函数开端履行 -> %d\n"),*x);
}
void father_thread()
{
DWORD dwThreadId;
int x = 5;
HANDLE hThread = CreateThread(NULL, 0,
(LPTHREAD_START_ROUTINE)Print,
&x, 0, &dwThreadId);
if (hThread == NULL)
{
_tprintf(TEXT("创立线程失利 -> %d\n"), GetLastError());
return 0;
}
//留意,我把下面一行代码注释掉了,先记住这行代码
//WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread);
return 0;
}
咱们在main函数中创立了一个子线程,这个子线程履行Print函数的代码,Print函数的功用很简单,仅仅一个一般的输出一段文本。前面说了,调用CreateThread函数的线程与CreateThread函数创立的线程是一起履行的,这就或许产生这样的状况:father_thread函数的线程现已完毕了,可是Print的线程还没有履行完毕。所以Print函数所拜访的x的值很或许现已产生了改变。据我试验发现,履行这段代码后,操控台上仅仅输出了“Print”,并没有把整句文本悉数输出。原因也很简单,正如前面所说,Windows是一个抢占式的多线程体系,在调用了Print函数后,father_thread函数并没有等候Print函数履行完毕,而是持续履行main的代码,这就导致还没有打印完文本整个进程就完毕了。要处理这个问题也很简单,把代码中的注释句子写入代码中就能够了。
这个问题也算是一个小小的坑吧
关于完毕线程
书中说到,完毕线程运转有四种办法:
- 线程函数自己回来(强烈引荐)
- 线程调用ExitThread来完毕自己
- 其它线程调用TerminateThread完毕线程
- 线程地点的进程完毕
原书作者引荐运用榜首种办法完毕线程,由于这样能够确保线程的资源能够被正确的开释;关于第二种办法,ExitThread函数会使线程中止运转,并整理该线程的操作体系资源,可是线程的C/C++资源不会被整理;关于第三种办法,TerminateThread函数是异步的,在TerminateThread函数回来时,它并不能确保所要完毕的线程必定中止了,其它的线程有或许还需求拜访要完毕的线程的仓库内存;最终一种办法相当于为每一个线程都调用了TerminateThread办法。
线程的完成细节
接下来咱们细心研究一下这张图片。首要咱们能够看到调用CreateThread导致体系创立了线程内核目标,该目标开端的运用计数器为2,由于被创立的线程自身算一个,CreateThread函数回来的句柄也算一个。该线程的其它特点也被初始化。
创立线程内核目标之后,体系在包括该线程的进程中为线程分配仓库。在仓库中,体系从高位地址到低位地址顺次将lpParameter和lpStartAddress写入仓库中(Windows更新了形参的称号,所以图中和这儿所说的不一样)。
每个线程都有自己的寄存器,称为线程的上下文(关于这部分的具体内容,能够参阅操作体系相关常识)。
线程的一切寄存器都保存在一个CONTEXT的结构中。咱们能够看到,指令指针寄存器指向了一个名为RtlUserThreadStart的函数,
这个函数便是线程开端履行的进口。RtlUserThreadStart函数的原型如下[^1]:
VOID RtlUserThreadStart(PTHREAD_START_ROUTINE lpStartAddress, LPVOID lpParameter);
[^1];这个函数现在在Windows协助文档里边现已找不到了,具体细节有待讲究
RtlUserThreadStart函数有两个参数,是由操作体系显式写入的。当新线程履行RtlUserThreadStart函数时,
会有如下工作产生:
- 环绕线程函数,会设置SEH,使得线程履行期间一切的反常都能够由体系默许处理
- 体系调用线程函数,把CreateThread函数的lpParameter参数传递给线程函数
- 线程函数回来时,RtlUserThreadStart调用ExitThread函数完毕线程
- 若线程产生了反常,RtlUserThreadStart会调用EixtProcess中止整个线程
留意以上履行进程,RtlUserThreadStart最终会调用ExitThread函数,着意味着RtlUserThreadStart函数永久不会退出,也便是说,RtlUserThreadStart函数永久不会回来。
最终要点:Windows线程API与C/C++运转时库
从_beginthreadex到_endthreadex
在之前,一个库别离有两个版别:单线程和多线程。规范C/C++运转库开端不是为多线程言语程序而规划的。
为了确保C/C++多线程应用程序正常运转,有必要创立一个数据结构与运用了C/C++运转库函数的线程相关联,这样在调用运转库函数时,这些函数会去查找主调线程的数据块,
防止影响其它线程。问题是,体系并不知道要在什么时候分配这种数据块,由于体系并不知道应用程序运用了C/C++运转库,也不知道咱们调用的函数是线程安全的。
记住,确保线程安满是程序员的职责。
直接运用CreateThread函数创立线程,不能确保在多线程环境下线程的安全,咱们经过调用_beginthreadex函数来处理这个问题。
这个函数的原型如下:
_MCRTIMP uintptr_t __cdecl _beginthreadex (
void *security,
unsigned stacksize,
unsigned (__CLR_OR_STD_CALL * initialcode) (void *),
void * argument,
unsigned createflag,
unsigned *thrdaddr
);
_beginthreadex函数对CreateThread函数进行了封装,_beginthreadex函数让每个线程都有一个专用的_tiddata内存块,这个内存块保存了传递给_beginthreadex的线程函数地址等数据
经过调查书中给出的代码,咱们发现在_beginethread里边,调用了CreateThread函数,而且传递给CreateThread函数的函数地址是_threadstartex,参数地址是_tiddata结构的地址。
接着,咱们进入了_threadstartex中履行代码。在_threadstartex函数中又调用TlsSetValue和_callthreadstartex函数。
然后,咱们跳转到_callthreadstartex函数中,在这个函数中又调用了_endthreadex函数,用来正确的处理_tiddata内存块
咱们是永久不会履行_threadstartex函数的回来句子的。
假如咱们直接调用CreateThread,当这个子线程需求_tiddata时,假如没有,则CreateThread创立一个揭露的_tiddata。
这会导致子线程在调用一些函数时,会让整个进程中止运转,而且,假如不调用_endthreadex完毕线程,
_tiddata内存块就不会被毁掉,然后导致内存走漏。
一起书中还主张,不要运用_endthread完毕线程。_endthread完毕线程之前,会调用CloseHand封闭线程句柄,
假如子线程现已完毕了,但父线程中运用了子线程的句柄,就会引发问题。所以咱们应该运用endthreadex完毕线程。
结语
本文对Windows线程机制进行了非常根本的解说,其间的更多细节,会在今后的博文中更新,感兴趣的读者敬请重视