创建线程
概述
- 每个进程至少都有一个线程。
- 线程的组成
- 线程的内核对象,操作系统用它管理线程。
系统还用内核对象来存放线程统计信息的地方。 - 线程栈,用于维护线程执行时所需的所有函数参数和局部变量。
- 线程的内核对象,操作系统用它管理线程。
- 进程从来不会执行任何东西,它是线程的容器。
线程必然是在某个进程的上下文中创建的,而且会在这个进程内部“终其一生”。
也就是说,线程要在进程的地址空间内执行代码和处理数据。 - 假如一个进程中有超过两个以上的线程,那这些线程将共享同一个地址空间。可以执行一样的代码,可以处理相同的数据。
另外,线程肯定是共享内核对象句柄的。因为句柄表是针对每一个进程的,而不是针对线程的。 - 所以,相比之下可以看出,进程占用系统资源更多,原因就是地址空间。
为一个进程创建一个虚拟的地址空间需要大量的系统资源。
系统中会发生大量的记录活动,需要用到大量的内存。而且,将.exe和.dll加载到地址空间,还需要用到文件资源。
什么时候创建线程
- 每次初始化进程时,系统都会创建一个主线程。
- 对于Microsoft C/C++编译器生成的应用程序,这个线程会首先执行C/C++运行库的代码,C/C++运行库的代码又会调用程序的入口点函数(如_tMain或_tWinMain),并继续执行。
直到入口点函数返回C/C++运行库的启动代码,C/C++运行库的代码才会调用ExitProcess。 - 对于很多应用程序而言,这个主线程是应用程序唯一需要的线程。但是根据场景,将应用程序设计成多线程的,可以使应用程序易于扩展。
编写线程函数
1 2 3 4 5 |
DWORD WINAPI ThreadProc(PVOID pvParam) { DWORD dwResult = 0; // ... return (dwResult); } |
- 线程函数可以执行我们希望它执行的任何任务。最后,线程函数会终止运行并返回。
此时,线程终止运行了,用于线程栈的内存也会被释放,线程内核对象的使用计数也会递减。如果使用计数变成0了,线程内核对象会被销毁。 - 类似进程内核对象,线程内核对象的寿命至少可以达到与它们相关联的线程那样长。而线程内核对象的寿命是可能超过线程本身的寿命长度的。
- 默认情况下,主线程的入口点函数必须命名为main,wmain,WinMain或wWinMain。但是,我们可以用/ENTRY:链接器选项来指定另一个函数作为入口点函数。
至于其他线程的函数可以任意命名。 - 主线程的入口点函数有字符串参数,所以它提供了ANSI/Unicode版本的给我们选择:main/wmain或WinMain/wWinMain。
线程函数只有一个参数,而且其意义由我们来定义。 - 线程函数必须提供一个返回值,这个返回值会成为该线程的退出代码。
- 线程函数应该尽可能使用函数参数或局部变量。
因为使用静态变量和全局变量时,多个线程可以同时访问这些变量,这会导致有可能破坏变量中保存的内容。
CreateThread
1 2 3 4 5 6 |
HANDLE CreateThread(PSECURITY_ATTRIBUTES psa, DWORD cbStackSize, PTHREAD_START_ROUTINE pfnStartAddr, PVOID pvParam, DWORD dwCreateFlags, PDWORD pdwThreadID); |
- 调用这个函数时,系统会创建一个线程内核对象。这个线程内核对象不是线程本身,而是一个较小的数据结构,操作系统用这个结构来管理线程。
- 系统从进程的地址空间中分配内存给线程栈使用。
- 新线程与负责创建的哪个线程在相同的进程上下文中运行。
因此,新线程可以访问进程内核对象的所有句柄、进程中的所有内存以及同一个进程中的其他线程的栈。
因此,同一个进程中的多个线程可以很容易地相互通信。
注意事项
- 这个函数是用于创建线程的Windows函数。
如果写的是C/C++代码,就绝对不要调用这个函数来创建线程。正确的选择是使用Microsoft C++运行库的函数_beginthreadex。
参数一
- 一个指向SECURITY_ATTRIBUTES结构体的指针。
如果是要使用线程内核对象的默认安全属性,则传入NULL。 - 如果是希望所有子进程都能继承到这个线程对象的句柄,那就必须指定一个SECURITY_ATTRIBUTES结构体,并将该结构体的bInheritHandle成员初始化为TRUE。
参数二
- 指定线程可以为其线程栈使用多少地址空间。
- 每个线程都有自己的线程栈。
至于主线程,是CreateProcess函数开始一个进程的时候,会在内部调用CreateThread来初始化进程的主线程。
主线程的这个参数,使用了保存在可执行文件内部的一个值。这个值可用链接器的/STACK开关来控制这个值。
1 2 3 4 |
// reserve用于设置系统将为线程预留多少地址空间,默认是1MB(在Itanium芯片组是4MB) // commit指定最初应为栈预留的地址空间区调拨多少物理存储空间,默认是1个页面(4kb) /STACK:[reserve] [,commit] |
- 随着线程中的代码开始执行,需要的存储空间可能不止1个页面,如果线程溢出它的栈,可能会发生异常。
系统会捕获这种异常,并为已预定的空间区域调拨另一个页面。这样,线程栈就可以根据需要动态的增大。 - 如果传入非0值,函数会为线程栈预定空间并为之调拨所需的所有存储空间。
如果传入的是0值,那么函数就会预定一个区域,并根据/STACK链接器开关指定的存储量来调拨存储器。 - 预留的地址空间的容量设定了栈空间的上限,这样才能捕获代码中的无穷递归bug。
参数三四
- 参数三指定了希望线程执行的线程函数的地址。
- 参数四是传给线程函数的参数。这个初始值可以是一个数值,也可以是一个指向数据结构的指针。
- 线程函数如果是不可重入函数,应该使用正确的线程同步技术。
可重入函数
- 可以在这个函数执行的任何时刻终端它,转入系统调度去执行另一段代码,而返回控制时不会出现什么错误。
- 也意味着这个函数除了使用自己栈上的变量外,不依赖任何环境,比如static变量。这样的函数就是pullcode可重入,可以允许有该函数的多个副本在运行,由于它们使用的是分离的栈(多线程),所以互不干扰。
不可重入函数
- 由于函数使用了一项系统资源,比如全局变量区、中断向量表等,如果它被中断的话,可能会出现问题,这类函数是不能运行在多任务环境下。
参数五
- 指定额外的标志来控制线程的创建。
- 如果值为0,线程创建之后立即就可以进行调度。
如果值为CREATE_SUSPENDED,系统将创建并初始化线程,但是会暂停该线程的运行,这样就让它无法调度。
参数六
- 必须是一个DWORD的有效地址,CreateThread函数用它来存储系统分配给新线程的ID。
- 传NULL值的话,就是告诉函数我们对该线程的线程ID没有兴趣。
终止线程
方法
- 线程函数返回(强烈推荐)
- 线程通过调用ExitThread函数“杀死”自己。(避免使用)
- 同一个进程或另一个进程中的其他线程调用TerminateThread函数(避免使用)
- 包含线程的进程终止运行(避免使用)
线程函数返回
- 设计线程函数时,应该确保在我们希望线程终止运行时,就让它们返回。这是保证线程的所有资源被正确释放的唯一方法。
- 线程函数返回,可以确保的清理工作:
- 线程函数中创建的所有C++对象都通过其析构函数被正确销毁。
- 操作系统正确释放线程栈使用的内存。
- 操作系统把线程的退出代码设为线程函数的返回值。
- 系统递减线程的内核对象的使用计数。
ExitThread
- 强迫线程终止运行。
1 |
VOID ExitThread(DWORD dwExitCode); |
- 该函数终止线程的运行,并导致操作系统清理该线程使用的所有操作系统资源。
但是,C/C++资源(如C++类对象)不会被销毁。
注意事项
- 这个函数是用于杀死线程的Windows函数。
如果写的是C/C++代码,就绝对不要调用这个函数来终止线程。正确的选择是使用Microsoft C++运行库的函数_endthreadex。
TerminateThread
- 这个函数可用于杀死任何一个线程。
1 2 |
BOOL TerminateThread(HANDLE hThread, DWORD dwExitCode); |
- 和ExitThread不同的是,ExitThread是杀死主调线程,而这个函数是可以杀死任何线程的。
- 线程的内核对象的使用计数会递减。
- 这个函数是异步的,在函数返回时,并不保证线程已经终止了。
参数一
- 要杀死的线程的句柄
参数二
- 退出代码
进程终止运行时
- 使用ExitProcess或TerminateProcess终止运行进程时,会终止运行在进程中的所有线程。
线程终止运行时
步骤
- 线程拥有的所有用户对象句柄会被释放。
- 在Windows中,大多数对象都是由包含了“创建这些对象的线程”的进程拥有的。但是一个线程有两个用户对象:窗口和钩子。
- 一个线程终止运行时,会自动销毁由线程创建或安装的任何窗口。
- 一个线程终止运行时,会自动卸载由线程创建或安装的钩子。
- 其他对象只有在拥有线程的进程终止时才被销毁。
- 线程的退出代码从STILL_ACTIVE变成传给ExitThread或TerminateThread的退出代码。
- 线程内核对象的状态变为触发状态。
- 如果线程是进程中的最后一个活动线程,系统认为进程也终止了。
- 线程内核对象的使用计数递减1。
其他
- 线程终止运行时,其关联的线程对象不会自动释放,除非这个对象所有未结束的引用都被关闭了
- 一旦线程不再运行了,系统中也没有其他线程再引用该线程的句柄了。
其他线程可以使用GetExitCodeThread来检查hThread所标识的那个线程是否已经终止运行,如果已经终止运行,可判断其退出代码是多少。
1 2 |
BOOL GetExitCodeThread(HANDLE hThread, PDWORD pdwExitCode); |
- 退出代码的值通过pdwExitCode返回,如果调用这个函数的时候 ,线程还没终止,那么pdwExitCode的值将会是STILL_ACTIVE标识符(0x103)。
本文为原创文章,版权归Aet所有,欢迎分享本文,转载请保留出处!
你可能也喜欢
- ♥ Dump分析:重复释放堆内存,死锁03/17
- ♥ Soui八06/20
- ♥ WindowsETW进程监控相关03/17
- ♥ Cef:沙盒、CefApp、CefClient02/29
- ♥ Windows 核心编程 _ 线程内幕07/06
- ♥ x86_64汇编学习记述一08/06