一、引言
要想掌握 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) | 需手动调用 _endthreadex 或 CloseHandle |
性能 | 与 _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*)¶m, 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;
}