Windows 提供了一套功能强大且完整的 API 来创建和管理线程,我们将从最基础的 CreateThread 函数讲起,然后介绍更现代、更安全的 _beginthreadex 函数,并深入探讨线程同步的关键概念,如互斥锁、事件、信号量等。

目录
- Windows 线程 API 概览
CreateThread: 基础线程创建函数_beginthreadex和_endthreadex: C/C++ 运行时库的线程函数 (推荐)
- 一个简单的多线程示例
- 线程同步
- 为什么需要同步? (竞态条件)
- 互斥量
- 事件
- 信号量
- 临界区 (轻量级互斥量)
- 线程通信
- 全局变量
- 结构体指针
- 完整示例:生产者-消费者模型
- 线程的生命周期与清理
- 最佳实践与注意事项
Windows 线程 API 概览
CreateThread (Windows API)
这是最底层的线程创建函数,由 Windows 内核提供。
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 );
- 返回值: 成功返回一个
HANDLE(线程句柄),失败返回NULL。 lpThreadAttributes: 线程安全属性,通常传NULL。dwStackSize: 线程栈大小,传0表示使用默认大小。lpStartAddress: 线程的起始地址,是一个函数指针,线程创建后,从这个函数开始执行。lpParameter: 传递给线程函数的参数,一个void*指针。dwCreationFlags: 创建标志,传0表示立即创建并运行;传CREATE_SUSPENDED表示创建后挂起,需要用ResumeThread唤醒。lpThreadId: 用于接收新线程的 ID,如果不需要可以传NULL。
重要警告: 在 C/C++ 程序中直接使用 CreateThread 可能会导致内存泄漏,因为 C 运行时库 的某些函数(如 malloc, fopen, strtok 等)内部使用静态数据来维护状态,当 CreateThread 创建的线程退出时,它不会自动清理这些静态数据,而主线程的 CRT 初始化代码也不会在新线程上运行,从而可能引发问题。
_beginthreadex 和 _endthreadex (C Runtime Library - CRT)
为了解决 CreateThread 的问题,微软提供了 CRT 版本的线程函数,它们在内部会正确初始化和清理 CRT 环境。
// 创建线程 uintptr_t _beginthreadex( [in, optional] void *security, [in] unsigned stack_size, [in] unsigned ( *start_address )( void * ), [in, optional] void *arglist, [in] unsigned initflag, [out] unsigned *thrdaddr ); // 结束线程 (在线程函数内部调用) void _endthreadex( unsigned retval );
- 返回值: 成功返回一个
uintptr_t(可以转换为HANDLE),失败返回0。 - 参数与
CreateThread类似,但函数签名和返回类型略有不同。 start_address: 是一个返回unsigned的函数指针。_endthreadex: 当线程函数执行完毕后,应该调用_endthreadex来退出线程,而不是return,这能确保 CRT 的资源被正确释放。
在 C/C++ 程序中,强烈推荐使用 _beginthreadex 而不是 CreateThread。

一个简单的多线程示例
下面是一个使用 _beginthreadex 创建两个线程的简单例子,一个线程打印 "Hello from Thread 1",另一个打印 "Hello from Thread 2"。
#include <stdio.h>
#include <windows.h>
#include <process.h> // _beginthreadex 的头文件
// 线程函数的返回类型必须是 unsigned (__stdcall *)
// 参数必须是 void*
unsigned __stdcall ThreadFunc(void* pArg) {
int threadNum = (int)(intptr_t)pArg; // 将 void* 转换回 int
printf("Hello from Thread %d (ID: %u)\n", threadNum, GetCurrentThreadId());
return 0; // 线程正常退出
}
int main() {
HANDLE hThreads[2];
unsigned threadIDs[2];
// 创建第一个线程
hThreads[0] = (HANDLE)_beginthreadex(NULL, 0, ThreadFunc, (void*)1, 0, &threadIDs[0]);
if (hThreads[0] == 0) {
printf("Failed to create thread 1. Error: %d\n", GetLastError());
return 1;
}
// 创建第二个线程
hThreads[1] = (HANDLE)_beginthreadex(NULL, 0, ThreadFunc, (void*)2, 0, &threadIDs[1]);
if (hThreads[1] == 0) {
printf("Failed to create thread 2. Error: %d\n", GetLastError());
return 1;
}
// 等待所有线程执行完毕
WaitForMultipleObjects(2, hThreads, TRUE, INFINITE);
// 关闭线程句柄
CloseHandle(hThreads[0]);
CloseHandle(hThreads[1]);
printf("All threads have finished.\n");
return 0;
}
编译: 在 Visual Studio 中直接编译即可,如果使用 MinGW (GCC) 命令行编译,需要链接 pthread 库(尽管我们用的是 Windows API,但 MinGW 可能需要):
gcc your_file.c -o your_program.exe -lws2_32 -lmsvcrt
线程同步
当多个线程访问共享资源(如全局变量、文件、硬件等)时,如果它们的执行顺序不确定,可能会导致数据不一致或程序崩溃,这就是竞态条件,同步机制用于控制线程的执行顺序,确保数据安全。
互斥量
互斥量就像一把钥匙,只有一个线程能拿到这把钥匙并进入“临界区”(访问共享资源的代码段),其他想进入的线程必须等待。

HANDLE hMutex; // 全局互斥量句柄 // 创建互斥量 hMutex = CreateMutex(NULL, FALSE, NULL); // 第二个参数 FALSE 表示初始状态为不拥有 // 在进入临界区前 WaitForSingleObject(hMutex, INFINITE); // 等待,直到获取到互斥量 // --- 临界区代码 --- // 访问共享资源 // --- // 离开临界区后 ReleaseMutex(hMutex); // 释放互斥量 // 使用完毕后关闭句柄 CloseHandle(hMutex);
事件
事件像一个信号灯,可以用来通知一个或多个某个事件已经发生。
- 自动重置事件: 一个等待的线程被释放后,事件会自动变回无信号状态。
- 手动重置事件: 所有等待的线程都会被释放,直到有人手动将其重置为无信号状态。
HANDLE hEvent; // 全局事件句柄 // 创建一个自动重置事件,初始状态为无信号 hEvent = CreateEvent(NULL, FALSE, FALSE, NULL); // 线程A (等待者) WaitForSingleObject(hEvent, INFINITE); // 会在这里阻塞,直到事件被触发 // 线程B (通知者) // ... 执行一些操作 ... SetEvent(hEvent); // 将事件设置为有信号,唤醒等待的线程 // 使用完毕后关闭句柄 CloseHandle(hEvent);
信号量
信号量像一个有固定数量停车位的停车场,它允许多个线程同时访问共享资源,但数量有限。
HANDLE hSemaphore; // 全局信号量句柄 // 创建一个信号量,初始资源数为2,最大资源数也为2 hSemaphore = CreateSemaphore(NULL, 2, 2, NULL); // 线程A WaitForSingleObject(hSemaphore, INFINITE); // 成功,资源数变为1 // ... 使用共享资源 ... ReleaseSemaphore(hSemaphore, 1, NULL); // 释放,资源数变回2 // 线程B 和 C 也可以同时获取,但如果再有 D,D 就必须等待。
临界区
临界区是进程内的同步机制,比互斥量更快,因为它只在用户模式下工作,不涉及内核对象,但它不能跨进程使用。
CRITICAL_SECTION cs; // 全局临界区对象 // 初始化临界区 InitializeCriticalSection(&cs); // 进入临界区 EnterCriticalSection(&cs); // --- 临界区代码 --- // 访问共享资源 // --- // 离开临界区 LeaveCriticalSection(&cs); // 删除临界区 (程序结束时) DeleteCriticalSection(&cs);
线程通信
线程之间最常见的通信方式是共享内存,即访问同一个全局变量或动态分配的内存。
- 传递简单数据: 通过
_beginthreadex的lpParameter参数传递。 - 传递复杂数据: 定义一个结构体,将结构体的指针作为
lpParameter传递。
示例:传递结构体
#include <stdio.h>
#include <windows.h>
#include <process.h>
typedef struct {
int id;
char* message;
} ThreadData;
unsigned __stdcall ThreadFunc(void* pArg) {
ThreadData* data = (ThreadData*)pArg;
printf("Thread %d: %s\n", data->id, data->message);
free(data->message); // 释放动态分配的字符串
free(data); // 释放结构体内存
return 0;
}
int main() {
HANDLE hThread;
unsigned threadID;
ThreadData* data1 = (ThreadData*)malloc(sizeof(ThreadData));
data1->id = 1;
data1->message = _strdup("Hello from dynamic data!"); // _strdup 会分配内存
hThread = (HANDLE)_beginthreadex(NULL, 0, ThreadFunc, data1, 0, &threadID);
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread);
return 0;
}
完整示例:生产者-消费者模型
这个经典模型展示了线程同步和通信的结合。
- 共享资源: 一个固定大小的缓冲区。
- 生产者线程: 不断向缓冲区放入数据。
- 消费者线程: 不断从缓冲区取出数据。
- 同步: 使用互斥量保护缓冲区,使用事件/信号量来通知缓冲区状态(空/满)。
#include <stdio.h>
#include <windows.h>
#include <process.h>
#define BUFFER_SIZE 5
int buffer[BUFFER_SIZE];
int in = 0, out = 0;
HANDLE hMutex, hEmptySlots, hFullSlots;
// 生产者
unsigned __stdcall Producer(void* pArg) {
for (int i = 0; i < 10; ++i) {
// 等待一个空槽位
WaitForSingleObject(hEmptySlots, INFINITE);
// 获取互斥锁以访问缓冲区
WaitForSingleObject(hMutex, INFINITE);
// --- 临界区:生产 ---
buffer[in] = i;
printf("Produced: %d at position %d\n", i, in);
in = (in + 1) % BUFFER_SIZE;
// ---
// 释放互斥锁
ReleaseMutex(hMutex);
// 通知消费者,有一个新槽位被填满了
ReleaseSemaphore(hFullSlots, 1, NULL);
}
return 0;
}
// 消费者
unsigned __stdcall Consumer(void* pArg) {
for (int i = 0; i < 10; ++i) {
// 等待一个满槽位
WaitForSingleObject(hFullSlots, INFINITE);
// 获取互斥锁以访问缓冲区
WaitForSingleObject(hMutex, INFINITE);
// --- 临界区:消费 ---
int item = buffer[out];
printf("Consumed: %d from position %d\n", item, out);
out = (out + 1) % BUFFER_SIZE;
// ---
// 释放互斥锁
ReleaseMutex(hMutex);
// 通知生产者,有一个空槽位了
ReleaseSemaphore(hEmptySlots, 1, NULL);
}
return 0;
}
int main() {
HANDLE hThreads[2];
// 创建同步对象
hMutex = CreateMutex(NULL, FALSE, NULL);
// 初始有 BUFFER_SIZE 个空槽位
hEmptySlots = CreateSemaphore(NULL, BUFFER_SIZE, BUFFER_SIZE, NULL);
// 初始有 0 个满槽位
hFullSlots = CreateSemaphore(NULL, 0, BUFFER_SIZE, NULL);
// 创建生产者和消费者线程
hThreads[0] = (HANDLE)_beginthreadex(NULL, 0, Producer, NULL, 0, NULL);
hThreads[1] = (HANDLE)_beginthreadex(NULL, 0, Consumer, NULL, 0, NULL);
// 等待线程结束
WaitForMultipleObjects(2, hThreads, TRUE, INFINITE);
// 清理
CloseHandle(hMutex);
CloseHandle(hEmptySlots);
CloseHandle(hFullSlots);
CloseHandle(hThreads[0]);
CloseHandle(hThreads[1]);
printf("Producer-Consumer simulation finished.\n");
return 0;
}
线程的生命周期与清理
- 正常退出: 线程函数执行到
return语句,或者调用_endthreadex。 - 强制终止: 极其不推荐使用
TerminateThread(hThread, 0),这会导致线程立即停止,不会执行任何清理操作(如关闭文件、释放内存),可能导致资源泄漏和数据损坏,只有在万不得已且确定不会造成严重后果的情况下才使用。 - 等待结束: 主线程应使用
WaitForSingleObject或WaitForMultipleObjects来等待工作线程完成,这可以确保所有工作都已完成。 - 关闭句柄: 线程结束后,其
HANDLE变为“无效句柄”,为了释放系统资源,主线程应该调用CloseHandle来关闭这些句柄。
最佳实践与注意事项
- 总是使用
_beginthreadex: 避免使用CreateThread以防止 CRT 相关的问题。 - 最小化临界区: 尽量让临界区的代码块尽可能小,以减少线程阻塞的时间。
- 避免死锁: 当一个线程持有锁A并等待锁B,而另一个线程持有锁B并等待锁A时,就会发生死锁,一个简单的预防方法是按固定顺序获取锁。
- 注意线程安全: 如果一个函数被多个线程调用,并且它依赖于静态或全局变量,那么它就不是线程安全的,要么避免在多线程环境中使用它,要么用锁来保护它。
- 小心参数传递: 传递给线程函数的参数(
lpParameter)在线程运行期间必须保持有效,如果参数是在栈上分配的局部变量,在线程函数访问它之前,主线程可能已经返回,导致栈被破坏,最佳实践是使用malloc动态分配内存,并在线程函数内部free它。 - 考虑使用更高层次的库: 对于非常复杂的并发任务,可以考虑使用更高层次的库,如 C++11 的
<thread>,<mutex>,<condition_variable>,或者 Intel 的 Threading Building Blocks (TBB),它们提供了更现代、更安全的抽象。
