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线程的函数很简单:

cpp 复制代码
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:启动一个线程

说的太干了。

cpp 复制代码
#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, 
		&params, 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 返回的线程句柄始终具有完全权限。

相关推荐
_Power_Y2 小时前
SSM面试题学习
java·开发语言·学习
拾光Ծ3 小时前
【C++】STL有序关联容器的双生花:set/multiset 和 map/multimap 使用指南
数据结构·c++·算法
爱写代码的小朋友3 小时前
生成式人工智能对学习生态的重构:从“辅助工具”到“依赖风险”的平衡难题
人工智能·学习·重构
澄澈i3 小时前
设计模式学习[20]---桥接模式
c++·学习·设计模式·桥接模式
A9better4 小时前
嵌入式开发学习日志35——stm32之超声波测距
stm32·单片机·嵌入式硬件·学习
TeleostNaCl4 小时前
如何在 Windows 上使用命令设置网卡的静态 IP 地址
网络·windows·经验分享·网络协议·tcp/ip·ip
青衫码上行4 小时前
【从0开始学习Java | 第18篇】集合(下 - Map部分)
java·学习
我星期八休息4 小时前
C++异常处理全面解析:从基础到应用
java·开发语言·c++·人工智能·python·架构