Windows 提供了一套强大的多线程 API,主要通过 windows.h 头文件来使用,下面我将从核心概念、关键 API、同步机制、完整示例以及最佳实践等方面进行详细说明。

(图片来源网络,侵删)
核心概念
在开始之前,理解几个基本概念很重要:
- 进程:一个正在运行的程序,它拥有独立的内存空间和系统资源。
- 线程:进程内的一个执行流,一个进程可以包含一个或多个线程,它们共享该进程的资源(如内存、文件句柄等),但每个线程有自己的调用栈和程序计数器。
- 并发:宏观上,多个任务看起来在同时运行,在单核 CPU 上,操作系统通过快速切换线程来实现“执行的假象。
- 并行:微观上,多个任务确实在同时运行,在多核 CPU 上,不同的线程可以被分配到不同的核心上同时执行。
关键 API 和数据结构
所有 Windows 多线程相关的函数和类型都定义在 windows.h 中。
1 创建线程
创建新线程使用 CreateThread 函数。
HANDLE CreateThread( [in, optional] LPSECURITY_ATTRIBUTES lpThreadAttributes, [in] SIZE_T dwStackSize, [in] LPTHREAD_START_ROUTINE lpStartAddress, [in, optional] LPVOID lpParameter, [in] DWORD dwCreationFlags, [out, optional] LPDWORD lpThreadId );
参数详解:

(图片来源网络,侵删)
lpThreadAttributes: 指向SECURITY_ATTRIBUTES结构的指针,通常设为NULL,表示使用默认安全属性。dwStackSize: 线程的初始栈大小,以字节为单位,如果为 0,则使用默认大小(通常是 1MB)。lpStartAddress: 线程的入口函数地址,这是一个函数指针,类型为LPTHREAD_START_ROUTINE,线程创建后,会立即从这个函数开始执行。lpParameter: 传递给线程入口函数的参数,这是一个void*类型的指针,你可以用它来传递任何数据结构(需要将结构体地址强制转换)。dwCreationFlags: 线程的创建标志。0: 线程创建后立即开始运行。CREATE_SUSPENDED: 线程创建后处于挂起状态,直到调用ResumeThread才会运行。
lpThreadId: 指向一个DWORD变量的指针,用于接收新线程的 ID,如果不需要,可以设为NULL。
返回值:
- 成功:返回一个
HANDLE类型的句柄,代表新创建的线程。 - 失败:返回
NULL,可以通过调用GetLastError()获取具体的错误代码。
2 线程入口函数
线程入口函数的原型是固定的:
DWORD WINAPI ThreadProc(LPVOID lpParam);
DWORD WINAPI: 返回类型和调用约定。LPVOID lpParam: 这是你在CreateThread中通过lpParameter传递进来的参数。
线程函数必须返回一个 DWORD 值,这个返回值可以通过 GetExitCodeThread 获取,通常用它来表示线程的执行状态或结果。
3 结束线程
- 线程正常退出:线程函数执行到
return语句。 - 主动终止线程(不推荐):使用
TerminateThread,这会导致线程立即停止,资源无法正确释放,可能造成内存泄漏和数据不一致,应尽量避免使用。 - 等待线程结束:使用
WaitForSingleObject或WaitForMultipleObjects来等待一个或多个线程执行完毕,这是最推荐的方式。
4 获取当前线程信息
GetCurrentThread(): 获取当前线程的伪句柄,这个句柄不能被子进程继承,也不能用于需要句柄句柄的操作(如等待)。GetCurrentThreadId(): 获取当前线程的 ID。
线程同步
多线程最大的挑战在于共享资源的访问,当多个线程同时读写同一个变量、同一个文件或同一个内存区域时,就会发生竞态条件,导致数据不一致或程序崩溃。

(图片来源网络,侵删)
Windows 提供了多种同步对象来解决这个问题。
1 临界区
临界区是最快的同步机制,但它只能在单个进程内使用。
- 工作方式:确保在任何时刻,只有一个线程可以进入被保护的代码区域。
- 缺点:不能用于跨进程同步。
主要函数:
InitializeCriticalSection(&CriticalSection): 初始化临界区。EnterCriticalSection(&CriticalSection)/TryEnterCriticalSection(&CriticalSection): 进入临界区,前者会阻塞,后者会立即返回。LeaveCriticalSection(&CriticalSection): 离开临界区。DeleteCriticalSection(&CriticalSection): 删除临界区,释放资源。
2 互斥量
互斥量与临界区类似,但它是内核对象,可以用于跨进程同步,因此速度比临界区慢。
- 工作方式:与临界区类似,但拥有所有权概念,只有获得互斥量的线程才能释放它。
- 优点:可用于进程间同步。
主要函数:
CreateMutex(NULL, FALSE, NULL): 创建一个互斥量。WaitForSingleObject(hMutex, INFINITE): 等待并获取互斥量所有权。ReleaseMutex(hMutex): 释放互斥量所有权。CloseHandle(hMutex): 关闭互斥量句柄。
3 事件
事件是一种通知机制,线程可以等待某个事件的发生。
- 工作方式:事件有两种状态:有信号 和 无信号,线程可以等待事件变为有信号状态。
- 类型:
- 自动重置事件:一个等待线程被释放后,事件会自动变为无信号状态。
- 手动重置事件:必须手动调用
ResetEvent才能将其变为无信号状态。
主要函数:
CreateEvent(NULL, FALSE, FALSE, NULL): 创建一个自动重置、初始状态为无信号的事件。SetEvent(hEvent): 将事件设置为有信号状态。ResetEvent(hEvent): 将事件设置为无信号状态。WaitForSingleObject(hEvent, INFINITE): 等待事件变为有信号状态。
完整示例代码
下面是一个结合了线程创建、参数传递、同步(临界区)和等待的完整示例。
#include <windows.h>
#include <stdio.h>
// 定义一个全局变量,作为共享资源
long g_totalCount = 0;
// 定义一个临界区对象
CRITICAL_SECTION g_cs;
// 线程函数
DWORD WINAPI ThreadProc(LPVOID lpParam) {
// 从参数中获取线程ID
int threadId = (int)lpParam;
int count = 0;
// 每个线程循环10万次
while (count < 100000) {
// 进入临界区,保护对 g_totalCount 的访问
EnterCriticalSection(&g_cs);
g_totalCount++; // 原子操作,但在这里为了演示同步,我们仍然保护
// 离开临界区
LeaveCriticalSection(&g_cs);
count++;
}
printf("Thread %d finished. It counted %d times.\n", threadId, count);
return 0; // 线程正常退出
}
int main() {
HANDLE hThreads[4]; // 用于保存线程句柄的数组
DWORD dwThreadId[4]; // 用于保存线程ID的数组
// 初始化临界区
InitializeCriticalSection(&g_cs);
printf("Main thread is creating 4 worker threads...\n");
// 创建4个线程
for (int i = 0; i < 4; i++) {
// 将线程ID作为参数传递给线程函数
hThreads[i] = CreateThread(
NULL, // 默认安全属性
0, // 默认堆栈大小
ThreadProc, // 线程函数
(LPVOID)(i + 1), // 传递给线程函数的参数 (线程ID)
0, // 立即运行
&dwThreadId[i] // 用于接收线程ID
);
if (hThreads[i] == NULL) {
printf("CreateThread failed (%d)\n", GetLastError());
// 清理已创建的线程
for (int j = 0; j < i; j++) {
CloseHandle(hThreads[j]);
}
DeleteCriticalSection(&g_cs);
return 1;
}
}
printf("Main thread is waiting for all worker threads to finish...\n");
// 等待所有线程执行完毕
WaitForMultipleObjects(4, hThreads, TRUE, INFINITE);
printf("All threads have finished. Final g_totalCount: %ld\n", g_totalCount);
// 清理资源
for (int i = 0; i < 4; i++) {
CloseHandle(hThreads[i]); // 关闭线程句柄
}
// 删除临界区
DeleteCriticalSection(&g_cs);
return 0;
}
代码解释:
- 全局变量
g_totalCount:这是一个共享资源,所有线程都会尝试修改它。 - 全局变量
g_cs:这是一个临界区对象,用于保护g_totalCount。 ThreadProc函数:这是每个线程的执行体,它接收一个线程ID作为参数,在一个循环中递增g_totalCount,在递增操作前后,使用EnterCriticalSection和LeaveCriticalSection来确保同一时间只有一个线程能修改g_totalCount。main函数:InitializeCriticalSection: 初始化临界区。- 循环调用
CreateThread创建4个线程,并将i+1作为线程ID传递。 WaitForMultipleObjects: 这是关键,它会让主线程阻塞,直到所有4个工作线程都执行完毕。- 关闭所有线程句柄并删除临界区,释放资源。
运行结果分析:
如果没有临界区,由于 g_totalCount++ 不是原子操作(它包含“读取-修改-写入”三步),g_totalCount 的值很可能不等于 4 * 100000 = 400000。加上临界区后,结果一定是 400000,因为它确保了操作的原子性。
最佳实践和注意事项
- 避免全局变量:虽然上面的例子用了全局变量,但在实际项目中,应尽量通过线程参数传递数据,或者使用线程本地存储来减少共享,降低耦合。
- 注意同步粒度:同步代码块(临界区/互斥量)的范围应尽可能小,只保护必要的共享资源,范围过大会降低并发性能。
- 防止死锁:死锁发生在两个或多个线程互相等待对方释放资源时,线程A持有锁1并等待锁2,而线程B持有锁2并等待锁1。避免死锁的原则是:
- 按固定的顺序获取多个锁。
- 尽量使用同一类型的同步原语。
- 设置超时等待(
WaitForSingleObject的dwMilliseconds参数不要总是用INFINITE)。
- 资源释放:
CreateThread创建的句柄必须用CloseHandle关闭。CreateMutex等创建的内核对象也必须关闭。malloc分配的在线程中使用的内存,其释放责任必须明确,避免内存泄漏或悬挂指针。 - 使用
try-finally或RAII模式:确保在异常或复杂逻辑下,LeaveCriticalSection或ReleaseMutex总是被调用,C语言中没有RAII,但你可以通过goto或在函数末尾添加finally代码块来模拟。 - 考虑使用 C11 线程库:如果你不依赖于 Windows 特有的 API,并且希望代码具有更好的可移植性,可以考虑使用 C11 标准引入的
<threads.h>,它提供了thrd_create,mtx_lock,cnd_wait等跨平台的线程和同步原语。
希望这份详细的指南能帮助你掌握 Windows C 语言多线程编程!
