浅谈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
);

其中各参数的意义在此不多赘述。接下来我们着重讨论一下lpStartAddresslpParameter参数。

lpStartAddresslpParameter参数

lpStartAddress

指向要由线程执行的应用程序定义函数的指针。 此指针表示线程的起始地址。

lpParameeter

指向要传递给线程的变量的指针。

从字面意思上理解这两个参数是不难的,问题在于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方法。

线程的实现细节

Windows下创建线程的细节
接下来我们仔细研究一下这张图片。首先我们可以看到调用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函数永远不会返回。

调用CreateThread

最后重点: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线程机制进行了十分基本的解释,其中的更多细节,会在以后的博文中更新,感兴趣的读者敬请关注

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/826439.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

校招回顾 | “青春不散场,梦想正起航”,极限科技(INFINI Labs)亮相湖北工业大学 2025 秋季校园招聘会

10 月 31 日,极限科技(INFINI Labs) 受邀参加 湖北工业大学 2025 届秋季校园招聘会,这不仅是一次与满怀激情的青年学子们的深度碰撞,更是一场关于青春与未来的美好邂逅。让我们一起回顾校招现场的精彩瞬间,重温那些闪耀的时刻。 一、梦想起航,共赴盛宴 怀揣着满满的诚意…

如何用 Spring AI + Ollama 构建生成式 AI 应用

为了构建生成式AI应用,需要完成两个部分:AI大模型服务:有两种方式实现,可以使用大厂的API,也可以自己部署,本文将采用ollama来构建 应用构建:调用AI大模型的能力实现业务逻辑,本文将采用Spring Boot + Spring AI来实现Ollama安装与使用进入官网:https://ollama.com/ ,…

MyBatis-Plus条件构造器:构建安全、高效的数据库查询

MyBatis-Plus 提供了一套强大的条件构造器(Wrapper),用于构建复杂的数据库查询条件。一、关于条件构造器(Wrapper) 1.1 简介 MyBatis-Plus 提供了一套强大的条件构造器(Wrapper),用于构建复杂的数据库查询条件。Wrapper 类允许开发者以链式调用的方式构造查询条件,无需编…

数据结构 - 图之代码实现

图遍历分为深度优先遍历(DFS)和广度优先遍历(BFS),DFS一直往下走直到没路再返回,BFS先走所有路一步。文章还介绍了以邻接矩阵存储无向图的实现方法,包括定义、初始化、获取点数量等操作。书接上回,我们继续来聊聊图的遍历与实现。01、遍历 在图的基本功能中有个很重要的…

CF1554E You

题面题解 注意a[u]是点u位置的a,不是每选一个点然后把非标记个数丢进vector里( 每选择一个点,相当于把相邻的非标记的边标为外向,最后一个点u的外向边个数就是a[u] 又观察发现每种边定向方案都可以构造(拓扑),所以一共有2^(n-1)种方案 设f[k]表示gcd=k,g[k]表示k|gcd,…

汽车虚拟仿真软件有哪些?行业软件大盘点!

汽车虚拟仿真可以大大提高汽车的研发效率和质量,降低成本和风险,增强汽车的竞争力和创新能力。本文将带领大家了解汽车虚拟仿真软件有哪些、汽车虚拟仿真实际应用以及汽车云交互实时渲染平台三个要点。汽车虚拟仿真是指利用计算机技术,根据汽车的设计、制造、测试、运行等各…

《机器学习》 学习记录 - 第四章

第4章 决策树 4.1 基本流程 决策树(decision tree)是一类常见的机器学习方法,也叫“判定树”。顾名思义,决策树是基于树的结构进行决策的。 一般的,一棵决策树包含一个根结点、若干个内部结点和若干个叶结点:叶结点对应于决策结果,其他每个结点则对应于一个属性测试; 每…

Fullcalendar

月光光关注作者注册登录 使用Fullcalendar管理时间计划调度安排月光光2020-01-05 阅读 4 分钟 Fullcalendar可以很好的管理日程安排事件,可以管理时间和任务调度,比如日常值班岗位安排、举办活动会议议程安排、项目行动安排、车间劳动岗位排班等等。今天我们来了解一下使用Fu…

哈希函数与数据完整性 (^=◕ᴥ◕=^)

哈希函数与数据完整性:保护猫咪世界的小鱼干 (^=◕ᴥ◕=^) 在数字世界中,我们总是希望确保传输和存储的数据没有被篡改,就像猫咪们想保护它们珍贵的小鱼干不被“偷吃”一样。为此,哈希函数(Hash Functions)成为了一个强大而可靠的工具。哈希函数能生成独特的数据“指纹”…

学期 2024-2025-1 学号 20241403 《计算机基础与程序设计》第六周学习总结

学期(如2024-2025-1) 学号(如:20241403) 《计算机基础与程序设计》第六周学习总结 作业信息这个作业属于哪个课程 <班级的链接>(如2024-2025-1-计算机基础与程序设计)这个作业要求在哪里 <作业要求的链接>(如2024-2025-1计算机基础与程序设计第一周作业)这个…

2024 强网杯逆向 Writeups

最心有余而力不足的一集,做完 vm 颈椎病犯了,第二天根本打。最后,加上学弟学妹打的,最后剩一个 Android 逆向没 AK,要是没有颈椎病这一说肯定 AK 了。感觉快退役了... mips 编译一个 qemu-6.2.0 mips-linux-user bindiff 一下恢复符号,怀疑修改了 ELF loader 或者 syscal…

坐标系-投影

墨卡托投影墨卡托投影(Mercator Projection),是正轴等角圆柱投影。由荷兰地图学家墨卡托(G.Mercator)于1569年创立。假设地球被围在一中空的圆柱里,其基准纬线与圆柱相切(赤道)接触,然后再假想地球中心有一盏灯,把球面上的图形投影到圆柱体上,再把圆柱体展开,这就是一…