这是一个非常核心的Windows多线程编程知识点,我会从以下几个方面为你进行说明:

- 函数简介:
CreateThread是什么? - 函数原型与参数详解:每个参数代表什么意思?
- 完整代码示例:如何创建并运行一个线程?
- 关键注意事项:为什么新线程要调用
ExitThread而不是return?为什么主线程要调用WaitForSingleObject? - 替代方案:为什么通常推荐使用
_beginthreadex而不是CreateThread?
函数简介
CreateThread 是 Windows API (Application Programming Interface) 提供的一个函数,它属于 Windows.h 头文件,它的作用是在当前进程的地址空间中创建一个新的执行线程。
当你调用这个函数成功后,操作系统会为你分配一个新的线程,让它和你的主线程(main 函数所在的线程)并发地执行代码,这对于执行耗时任务(如文件下载、复杂计算)而不阻塞主线程(保持UI响应)非常有用。
函数原型与参数详解
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 |
LPSECURITY_ATTRIBUTES |
安全属性指针,通常传 NULL,表示使用默认安全描述符。 |
dwStackSize |
SIZE_T |
线程的初始堆栈大小(字节),如果为 0,则使用默认大小(通常是 1MB)。 |
lpStartAddress |
LPTHREAD_START_ROUTINE |
最重要的参数,这是一个函数指针,指向新线程开始执行的函数,这个函数必须遵循特定的签名:DWORD WINAPI ThreadFunc(LPVOID lpParam); |
lpParameter |
LPVOID |
传递给新线程函数的参数,你可以通过这个参数向新线程传递任何数据(指针、结构体等),如果不需要参数,可以传 NULL。 |
dwCreationFlags |
DWORD |
控制线程创建的标志,常用值: - 0:创建后立即运行。- CREATE_SUSPENDED:创建后挂起,直到调用 ResumeThread 才会运行。 |
lpThreadId |
LPDWORD |
一个指向 DWORD 类型的变量,用于接收新线程的ID,如果不需要,可以传 NULL。 |
返回值:

- 成功:返回一个
HANDLE类型的句柄,代表新创建的线程,你需要保存这个句柄,以便后续管理线程(如等待、关闭)。 - 失败:返回
NULL,可以通过调用GetLastError()函数获取具体的错误码。
完整代码示例
下面是一个完整的、可运行的C语言示例,它演示了如何创建一个线程,让它打印数字,然后主线程等待它完成。
#include <windows.h>
#include <stdio.h>
// 1. 定义线程函数的签名
// 线程函数必须返回一个 DWORD WINAPI,并且接收一个 LPVOID 参数
DWORD WINAPI ThreadFunc(LPVOID lpParam)
{
// lpParam 是主线程传递过来的参数
int* pNum = (int*)lpParam;
int count = *pNum;
printf("新线程启动!将打印 %d 次,\n", count);
for (int i = 0; i < count; i++) {
printf("线程输出: %d\n", i + 1);
// 模拟耗时操作
Sleep(500); // 休眠500毫秒
}
printf("新线程结束!\n");
// 2. 线程函数结束时,必须调用 ExitThread 或 return
// return 在这里等同于 ExitThread
return 0;
}
int main()
{
HANDLE hThread; // 用于存储线程句柄
DWORD dwThreadId; // 用于存储线程ID
int threadParam = 5; // 传递给线程的参数
printf("主线程:准备创建新线程...\n");
// 3. 调用 CreateThread 创建线程
hThread = CreateThread(
NULL, // 默认安全属性
0, // 默认堆栈大小
ThreadFunc, // 线程函数的地址
&threadParam, // 传递给线程函数的参数
0, // 立即运行
&dwThreadId // 获取线程ID
);
// 检查线程是否创建成功
if (hThread == NULL) {
printf("创建线程失败!错误码: %d\n", GetLastError());
return 1;
}
printf("主线程:新线程创建成功!线程ID: %d\n", dwThreadId);
printf("主线程:现在主线程和新线程将同时运行...\n");
// 4. 主线程等待新线程结束
// WaitForSingleObject 会阻塞主线程,直到指定的对象(这里是我们的线程) signaled(结束或收到信号)
WaitForSingleObject(hThread, INFINITE);
printf("主线程:检测到新线程已结束,主线程继续执行,\n");
// 5. 关闭线程句柄
// 使用完句柄后,必须调用 CloseHandle 释放资源
CloseHandle(hThread);
printf("主线程:程序结束,\n");
return 0;
}
如何编译和运行:
- 将代码保存为
thread_example.c。 - 打开 Visual Studio 的开发者命令提示 (x64 Native Tools Command Prompt)。
- 使用以下命令进行编译:
cl thread_example.c /link
- 运行生成的
thread_example.exe。
预期输出: 你会看到主线程和新线程的输出是交错进行的,这证明了它们是并发执行的。
主线程:准备创建新线程...
主线程:新线程创建成功!线程ID: 1234 // ID是随机的
主线程:现在主线程和新线程将同时运行...
新线程启动!将打印 5 次。
线程输出: 1
线程输出: 2
线程输出: 3
线程输出: 4
线程输出: 5
新线程结束!
主线程:检测到新线程已结束,主线程继续执行。
主线程:程序结束。
关键注意事项
a. 线程函数的返回:ExitThread vs return
在C/C++中,在线程函数内部使用 return 和调用 ExitThread() 效果是一样的,有一个非常重要的区别:

return:会调用该线程函数作用域内所有局部对象的析构函数。ExitThread():不会调用任何局部对象的析构函数。
这可能导致资源泄漏(如没有关闭的文件句柄、没有释放的内存等),最佳实践是:
- 在线程函数中,直接
return即可,这样更符合C++的习惯,能保证局部对象被正确销毁。 ExitThread主要用于在线程函数外部强制终止一个线程(不推荐,因为可能导致资源泄漏和程序不稳定)。
b. 主线程等待:WaitForSingleObject
为什么主线程需要等待新线程结束?
如果不等待,主线程可能在执行完 printf("主线程:程序结束,\n"); 后就退出了,当主线程退出时,整个进程就会被终止,导致新线程被“强行杀死”,它还没来得及完成的操作就会中断。
WaitForSingleObject(hThread, INFINITE); 的作用就是告诉主线程:“在这里等我,直到 hThread 代表的那个线程执行完毕你再继续走”,这确保了新线程有足够的时间完成它的任务。
c. 关闭句柄:CloseHandle
CreateThread 返回的 HANDLE 是一个内核对象,它不是线程本身,而是操作系统用来管理线程的一个“把手”或“引用”,当你不再需要这个句柄时(比如已经等待线程结束),必须调用 CloseHandle 来释放它,否则,会造成句柄泄漏,长期运行的程序可能会因为句柄耗尽而崩溃。
替代方案:_beginthreadex vs CreateThread
在C语言标准库(C Runtime Library, CRT)中,有一个功能与 CreateThread 非常相似的函数:_beginthreadex。
强烈推荐在C/C++程序中使用 _beginthreadex 而不是 CreateThread。
原因:C/C++运行时库的内部数据
C/C++运行时库(如 malloc, free, strtok, _crt_debugger_hook 等)使用了一些静态的或线程局部存储的数据来管理自己的状态。CreateThread 是一个纯粹的Windows API,它不知道这些CRT内部数据的存在。
当你使用 CreateThread 创建一个线程时:
- 新线程开始运行。
- 如果新线程调用了任何CRT函数(
printf),这些函数可能会去访问或修改CRT的内部数据。 - 如果多个线程同时访问这些共享数据,就会导致数据竞争和不可预测的行为(程序崩溃、数据损坏等)。
_beginthreadex 的作用正是在创建线程之前,初始化这些CRT内部数据,为新线程创建一份独立的“上下文”,使用 _beginthreadex 可以保证CRT函数在线程环境下的安全性。
_beginthreadex 的用法与 CreateThread 几乎完全一样,只是参数和返回值类型略有不同(它返回一个 uintptr_t,但可以安全地转换为 HANDLE)。
使用 _beginthreadex 的示例
#include <windows.h>
#include <stdio.h>
#include <process.h> // 包含 _beginthreadex
// 线程函数签名不变
DWORD WINAPI ThreadFunc(LPVOID lpParam)
{
int* pNum = (int*)lpParam;
int count = *pNum;
printf("新线程启动!将打印 %d 次,\n", count);
for (int i = 0; i < count; i++) {
printf("线程输出: %d\n", i + 1);
Sleep(500);
}
printf("新线程结束!\n");
return 0;
}
int main()
{
HANDLE hThread;
unsigned int dwThreadId; // 注意这里是 unsigned int
int threadParam = 5;
printf("主线程:准备创建新线程...\n");
// 使用 _beginthreadex
hThread = (HANDLE)_beginthreadex(
NULL, // 默认安全属性
0, // 默认堆栈大小
(unsigned int (__stdcall*)(void*))ThreadFunc, // 注意函数指针转换
&threadParam, // 传递给线程函数的参数
0, // 立即运行
&dwThreadId // 获取线程ID
);
if (hThread == 0) { // 注意:失败时返回0
printf("创建线程失败!错误码: %d\n", GetLastError());
return 1;
}
printf("主线程:新线程创建成功!线程ID: %u\n", dwThreadId);
printf("主线程:现在主线程和新线程将同时运行...\n");
WaitForSingleObject(hThread, INFINITE);
printf("主线程:检测到新线程已结束,主线程继续执行,\n");
// 同样需要关闭句柄
CloseHandle(hThread);
printf("主线程:程序结束,\n");
return 0;
}
| 特性 | CreateThread (Windows API) |
_beginthreadex (CRT) |
|---|---|---|
| 来源 | Windows.h |
process.h |
| 安全性 | 不安全,可能导致CRT内部数据竞争 | 安全,正确初始化了CRT环境 |
| 推荐度 | 不推荐在C/C++中使用 | 强烈推荐 |
| 线程结束 | 可使用 return |
必须调用 _endthreadex 或 return |
| 资源清理 | 需要手动 CloseHandle |
需要手动 CloseHandle |
核心要点:
CreateThread是Windows API,用于创建线程。- 线程函数必须返回
DWORD WINAPI并接收LPVOID参数。 - 主线程通常需要使用
WaitForSingleObject等待子线程结束,以避免进程提前终止。 - 使用完句柄后,必须调用
CloseHandle释放资源。 - 在C/C++编程中,请优先使用
_beginthreadex以确保与C运行时库的兼容性和安全性。
