Windows 线程的创建和销毁

Posted by KalosAner on January 24, 2025

一、引言

要想掌握 Windows 平台下的线程,应首先理解“内核对象”(Kernel Objects)的概念。

操作系统创建的资源(Resource)有很多种,如进程、线程、文件和即将介绍的信号量、互斥量等。其中大部分都是通过程序员的请求创建的,虽然文请求方式不同(请求中使用的函数)各不相同,但是它们都是由 Windows 操作系统创建并管理的资源。操作系统为了记录相关信息的方式以管理各种资源,在其内部生成数据块格式(可以看作结构体变量)。每种资源拥有的数据块格式也有不同,这类数据块称为“内核对象”。

内核对象创建者和所有者均为操作系统,创建、管理、销毁时机的决定等工作均由操作系统完成。

二、线程创建

函数原型

#include <windows.h>

HANDLE CreateThread(
	LPSECURITY_ATTRIBUTES lpThreadAttributes,
    SIZE_T dwStackSize,
    LPTHREAD_START_ROUTINE lpStartAddress,
    LPVOID lpParameter,
    DWORD dwCreationFlags,
    LPDWORD lpThreadId
);

lpThreadAttributes:线程安全相关信息,使用默认设置时传递 NULL。

dwStackSize:要分配给线程的栈大小,传递 0 时生成默认大小的栈。

lpStartAddress:函数指针,作为线程函数,并且格式为 DWORD WINAPI ThreadFunc(LPVOID lpParam); 或者 void ThreadFunc()

lpParameter:传递线程函数信息。

dwCreationFlags:用于指定线程创建后的行为,传递 0 时,线程创建后立即进入可执行状态。

lpThreadId:传出参数,用于保存线程 ID 的变量地址值。

成功时返回线程句柄(相当于 Linux 的文件描述),失败时返回 NULL。

但是 CreateThread 函数创建出的线程在使用 C/C++ 标准函数时并不稳定。如果线程要调用 C/C++ 标准函数通常使用如下方法:

1
2
3
4
5
6
7
8
9
10
11
#include <process.h>

// 参数与 CreateThread 的参数一一对应
uintptr_t _beginthreadex(
	void * security,
	unsigned stack_size,
	unsigned (* start_address)(void *),
	void * arglist,
	unsigned initflag,
	unsigned * thrdaddr
);

_beginthreadex 之前有 _beginthread 函数,但是 _beginthread 函数会让创建线程时返回的句柄失效,以防止访问内核对象。 _beginthreadex 就是为了解决这一问题而定义的函数。

除此之外,C++ 11 推出了 std::thread 支持跨平台,并且性能与 _beginthreadex 相当。

特性 std::thread (C++11 标准库) _beginthreadex (Windows CRT)
跨平台性 支持跨平台(Linux/macOS/Windows) 仅限 Windows 平台
代码简洁性 封装度高,无需手动管理句柄和资源 需显式关闭句柄(CloseHandle
兼容性 需 C++11 及以上编译器支持 兼容旧版 C/C++ 代码和 CRT 库
资源管理 自动释放线程资源(RAII) 需手动调用 _endthreadexCloseHandle
性能 _beginthreadex 性能接近 直接调用 Windows API,略微轻量

与 Linux 相同,Windows 同样在 main 函数返回后终止进程,也同时终止其中包含的所有线程。可以通过特殊方法解决该问题。

三、线程销毁

线程内核对象会记录线程状态,当线程内核对象需要重点关注线程是否已终止,终止状态会标记为 “signaled 状态” ,未终止状态标记为 “non-signaled 状态”。

在线程销毁之前就当判断线程是否终止,可以使用 WaitForSingleObject 函数进行判断。

函数原型:

1
2
3
#include <windows.h>

DWORD WaitForSingleObjext(HANDLE hHandle, DWORD dwMilliseconds);

hHandle:线程句柄

dwMilliseconds:阻塞 dwMilliseconds 毫秒,传递 INFINITE 则一直阻塞。

返回值:signaled 返回 WAIT_OBJECT_0,超时返回 WAIT_TIMEOUT

除此之外还有一个函数可以同时判断多个线程的状态:

1
2
3
#include <windows.h>

DWORD WaitForMultipleObjects(DWORD nCount, const HANDLE * lpHandles, BOOL bWaitAll, DWORD dwMilliseconds);

nCount:内核对象数

lpHandles:存有内核对象句柄的数组地址值

bWaitAll:如果为 TRUE,则所有内核对象全部变为 signaled 时返回;如果为 FALSE,则只要有 1 个验证对象的状态变为 signaled 就会返回。

dwMilliseconds:阻塞 dwMilliseconds 毫秒,传递 INFINITE 则一直阻塞。

这两个函数也可以用来处理事件对象。

CloseHandle 是 Windows API 中用于关闭内核对象句柄的核心函数,其函数原型如下:

1
2
3
BOOL CloseHandle(
  HANDLE hObject
);

Object:需要关闭的已打开内核对象句柄,例如线程、进程、文件、事件等。

返回:

  • 非零值(TRUE):表示关闭成功。
  • 零(FALSE):表示失败,可通过 GetLastError() 获取错误代码。

调用 CloseHandle 后,系统会将句柄对应的内核对象引用计数减 1。当引用计数归零时,对象会被系统彻底删除。

四、代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <stdio.h>
#include <windows.h>
#include <process.h>

unsigned WINAPI ThreadFunc(void* arg)
{
	int i;
	int cnt = *((int*)arg);
	for (i = 0; i < cnt; ++i) {
		Sleep(1000);
		puts("running thread");
	}
	return 0;
}
int main(int argc, char* argv[]) {
	HANDLE hThread;
	unsigned threadID;
	int param = 5;

	hThread = (HANDLE)_beginthreadex(NULL, 0, ThreadFunc, (void*)&param, 0, &threadID);
	if (hThread == 0) {
		printf("Thread creation failed\n");
		return -1;
	}
	// Wait for the thread to finish
	WaitForSingleObject(hThread, INFINITE);
	CloseHandle(hThread);
	printf("Thread finished\n");
	return 0;
}