Windows 10 系统编程——线程专题1
Windows 10 系统编程——线程专题1
前言
前面我们已经仔细了解了一下Windows的进程。现在我们要准备进一步详细的学习线程。线程的话题非常的庞大。我们回顾一下操作系统中对于进程和线程的描述,这样我们才会进一步的理解我们要学习什么。
任何一本操作系统的教程都逃不了进程和线程这两个概念。笔者自己学习的时候就混淆过这两个概念。我们单拎出来讲:
- 进程是资源分配的基本单位,它为程序的运行提供了独立的内存空间和系统资源
- (现代操作系统中)线程则是处理器调度的基本单位,它代表了进程中的一个执行流。
看到区别了嘛?进程像是一个容器,给我们的线程提供资源以运行。进程自身负责管理代码、数据和资源。线程才是那个真正运行代码的主角,他负责真正推动指令的运行。一个进程至少会有一个主线程来执行任务,也可以根据需要创建多个线程并行工作。我们这篇博客的核心在这里
由于线程共享进程的资源,因此相比于进程,线程切换和通信的成本更低,这让多线程成为现代软件提升响应速度和并发能力的核心手段。但与此同时,共享也意味着风险:线程之间必须通过同步机制来避免数据竞争,否则就会引发难以排查的错误。不担心,我们之后会详细谈到这个内容
正因为如此,学习线程时,我们不仅要理解它与进程的关系,更要掌握线程带来的优势与挑战。只有在清楚进程与线程各自特性的基础上,我们才能更好地进入 Windows 线程的世界,去理解它是如何被创建、调度以及管理的。
Windows的线程
Windows的线程是我们关注的要点。打开你的任务管理器,切换到性能CPU的视图,咱们就能看到我们感兴趣的线程(此时此刻,Windows中的线程有4695个)
我们回来,线程是一个真正执行代码的家伙,我们又知道,一段代码隶属于下面两种类型:
- CPU 密集型操作——依赖 CPU 操作才能完成的计算或函数调用。
- I/O 密集型操作——针对 I/O 设备(例如磁盘或网络)执行的操作。
CPU密集型是这样的任务,它会频繁的使用CPU完成工作,代表性的就是基于数学运算的计算类工作;IO 密集型操作更多的是说的跟其他外设打交道的部分。比如说经典的有发起和等待网络请求,向文件系统(可能是磁盘或者是其他什么东西)写入和读取内容。这些操作显然跟计算半毛钱关系没有,因此跟CPU关系不大。同步的执行这些代码,只会让CPU睡大觉。所以也就意味着CPU应当脱开身去执行其他内容。
创建线程
单刀直入,Windows创建线程比进程容易得多,而且资源开销小的多。创建Windows线程的函数很简单:
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
);
参数看着有点费劲,快速说一下:要求提供线程的属性,栈大小,执行的函数和参数,创建的标志位和如果需要的话,内参会返回线程的ID。
[in, optional] lpThreadAttributes
指定新线程的安全描述符,并确定子进程是否可以继承返回的句柄。 如果 lpThreadAttributes 为 NULL,则线程将获取默认的安全描述符,并且无法继承句柄。 线程的默认安全描述符中的访问控制列表(ACL)来自创建者的主要令牌。一般而言,咱们都是传递NULL的。
[in] dwStackSize
堆栈的初始大小(以字节为单位)。 系统将此值舍入到最近的页面。 如果此参数为零,则新线程使用可执行文件的默认大小。 所以默认的情况下,咱们会传递0,表达的是使用默认的值。
[in] lpStartAddress
指向由线程执行的应用程序定义函数的指针。 此指针表示线程的起始地址。 这个不神秘,我们知道,函数的地址指向的就是标记该子程序的第一个机器码指令所在的位置。熟悉汇编的同志们立马就能知道说的就是写汇编程序的时候我们打上的标记tag处被安排的地址——call那个地址的指令需要提供那个指令的地址,这里是一回事情,只不过C语言这里就是传递函数指针了。我们要求函数的签名格式必须是——接受一个指向参数的指针(这就很有意思了,我们需要保证传递到线程解引用的时候,这些参数都必须是有效的),返回一个DWORD表示线程的执行状态结果。这就跟咱们的Main返回0表示正常其他值表示异常是一个道理。
DWORD WINAPI ThreadProc(_In_ LPVOID lpParameter
);
[in, optional] lpParameter
指向要传递给线程的变量的指针。
[in] dwCreationFlags
控制线程创建的标志。
名称 | 值 | 意义 |
---|---|---|
0 | 0 | 创建后,线程会立即运行 |
CREATE_SUSPENDED | 0x00000004 | 线程以挂起状态创建,我们需要手动放下来(咱们后面会提到ResumeThread) |
STACK_SIZE_PARAM_IS_A_RESERVATION | 0x00010000 | dwStackSize 参数 指定堆栈的初始保留大小。 如果未指定此标志,dwStackSize 指定提交大小。 |
[out, optional] lpThreadId
指向接收线程标识符的变量的指针。 如果此参数 NULL,则不返回线程标识符。
返回值
如果函数成功,则返回值是新线程的句柄。如果函数失败,则返回值 NULL。
请注意,即使 lpStartAddress 指向数据、代码或无法访问,CreateThread 也可能成功。 如果线程运行时起始地址无效,则会发生异常,线程将终止。 由于启动地址无效,线程终止作为线程进程的错误退出进行处理。
样例1:启动一个线程
说的太干了。
#include <Windows.h>
#include <iostream>struct ThreadFuncParams {int params;
};DWORD WINAPI threadFunc(LPVOID params) {std::cout << "OK, we get" << ((ThreadFuncParams*)params)->params << std::endl;Sleep(1000);std::cout << "Job Finished!\n";return 0;
}int main()
{ThreadFuncParams params;params.params = 42;HANDLE hThread = CreateThread(nullptr, 4096, threadFunc, ¶ms, 0, nullptr);if (!hThread) {std::cout << "Create Thread failed!";return -1;}WaitForSingleObject(hThread, INFINITE);DWORD result = -1;GetExitCodeThread(hThread, &result);std::cout << "Result has been exited! " << result << "\n";CloseHandle(hThread);return 0;
}
WaitForSingleObject
表达的是等待目标句柄完成工作。这里就是等待线程执行结束的意思,没啥其他的含义。GetExitCodeThread
被拿来获取线程的返回值。
终止一个线程
⚠ 下面的API微软今天都不提倡了!除非有正当理由,不要使用下面的任何一个API来停止线程。相反,使用返回退出码的方式让线程退出
每个好线程(或坏线程)最终都会结束。线程终止的方式有三种:
- 线程函数返回(最佳选择)
- 线程调用 ExitThread(最好避免)
- 使用 TerminateThread 终止线程(最好避免)
最佳选择是直接从线程函数返回。当线程开始执行时,线程函数实际上并不是线程执行的第一个或唯一一个函数。实际上,线程是在一个名为 RtlUserThreadStart 的 NTDLL.dll 函数中开始执行的,该函数从概念上讲,会调用提供给 CreateThread 的线程实际函数。一旦线程函数返回,RtlUserThreadStart 会进行一些清理工作并调用 ExitThread。请注意,ExitThread只能由线程调用来终止自身,正如其原型所示:
void ExitThread(_In_ DWORD exitCode);
Kernel32.dll 中的 ExitThread 实际上是 NtDll.Dll 中 RtlExitUserThread 的转发器。从线程函数显式调用 ExitThread 的问题至少在于,由于 ExitThread 永远不会返回,因此不会调用 C++ 析构函数。因此,最好直接从线程函数返回,以便它能够正确清理本地 C++ 对象。
无论如何,ExitThread 还会使用 DLL_THREAD_DETACH 原因参数为进程中的所有 DLL 调用 DllMain 函数。这允许 DLL 执行每个线程的操作。例如,DLL 可以分配一些内存块来基于每个线程管理某些内容。
终止线程的第三种方法是从另一个线程(即使属于另一个进程)调用 TerminateThread。唯一的条件是调用者能够获得带有 THREAD_TERMINATE 访问掩码的线程句柄。TerminateThread 的定义如下:
BOOL WINAPI TerminateThread(
_Inout_ HANDLE hThread,
_In_ DWORD dwExitCode);
使用此调用终止线程几乎总是一个坏主意。问题在于线程已经完成了哪些操作,以及由于终止而尚未完成哪些操作。如果线程在执行实际工作时终止,则无法判断它执行了哪些指令,以及由于终止而无法执行哪些其他代码。应用程序可能处于不一致的状态。举一个极端(但并非不可能)的例子,线程可能已经获取了一个临界区,但没有机会释放它,从而导致死锁,因为其他等待该临界区的线程将永远等待。
TerminateThread 的另一个问题是它不会使用 DLL_THREAD_DETACH 调用 DLL 的 DllMain 函数。这意味着 DLL 无法运行某些可能释放内存或执行其他操作的代码,从而撤销线程创建时的操作。TerminateThread 的这些问题意味着安全地调用此函数的情况很少见,应该有更好的方法来处理任何似乎需要它的情况。不过,如果需要这样做,调用者必须获取一个具有 THREAD_TERMINATE 访问权限且足够强大的句柄。CreateThread 和 CreateProcess 返回的线程句柄始终具有完全权限。
建时的操作。TerminateThread 的这些问题意味着安全地调用此函数的情况很少见,应该有更好的方法来处理任何似乎需要它的情况。不过,如果需要这样做,调用者必须获取一个具有 THREAD_TERMINATE 访问权限且足够强大的句柄。CreateThread 和 CreateProcess 返回的线程句柄始终具有完全权限。