为什么需要 _beginthread 而不是 CreateThread?
这是理解 _beginthread 的核心,在 Windows 平台上,标准的 C/C++ 运行时库(CRT)为了支持像文件 I/O、字符串操作、内存分配(malloc/free)等功能,会使用一些“线程本地存储”(Thread-Local Storage, TLS)来存储每个线程的状态信息,

errno:错误码。strerror:错误信息缓存。- 文件句柄表。
- 内存分配器(
malloc)的状态。
当你使用 Windows API 的 CreateThread 函数创建一个新线程时,这个新线程并不会自动初始化这些 CRT 所需的 TLS 数据,它只会继承调用线程的堆栈,但不会继承 CRT 的“上下文”。
这会导致什么问题?
如果在由 CreateThread 创建的线程中调用了任何 CRT 函数(这几乎是不可避免的),printf、malloc 或 fopen,线程可能会访问到未初始化或错误的 TLS 数据,从而引发不可预测的行为,最常见的后果就是:
- 内存泄漏:
malloc和free的内部状态混乱,导致内存无法正确释放。 - 程序崩溃:访问无效的 TLS 数据导致访问冲突。
- 数据损坏:
errno或其他状态被错误地修改。
_beginthread 和 _beginthreadex 的作用
_beginthread 和 _beginthreadex 就是为了解决这个问题而设计的,它们是 CRT 提供的函数,在创建新线程的同时,会自动初始化该线程所需的所有 CRT TLS 数据。

它们是 CreateThread 的“安全封装”,确保了新线程能够正确地使用 C/C++ 运行时库。
_beginthread
这是较旧、较简单的版本,功能有限。
- 函数原型:
uintptr_t _beginthread( void (__cdecl *start_address)(void *), unsigned stack_size, void *arglist ); - 参数:
start_address: 线程的起始函数,必须是一个__cdecl调用约定的函数,接受一个void*参数。stack_size: 线程的堆栈大小,如果为 0,则使用默认大小。arglist: 传递给线程函数的参数。
- 返回值:
- 成功:返回新线程的句柄(类型为
uintptr_t)。 - 失败:返回 -1。
- 成功:返回新线程的句柄(类型为
- 特点:
- 简单易用。
- 线程函数必须是
__cdecl调用约定。 - 无法获取线程的退出码。
- 无法设置线程的安全属性。
- 无法指定线程的创建标志(如是否立即运行)。
_beginthreadex
这是更现代、更强大的版本,是 _beginthread 的超集,也是推荐使用的版本,它的设计更接近于 Windows API 的 CreateThread。
- 函数原型:
uintptr_t _beginthreadex( void *security, unsigned stack_size, unsigned (__stdcall *start_address)(void *), void *arglist, unsigned initflag, unsigned *thrdaddr ); - 参数:
security: 指向SECURITY_ATTRIBUTES结构的指针,用于控制线程句柄的继承,可以为NULL。stack_size: 线程的堆栈大小,如果为 0,则使用默认大小。start_address: 线程的起始函数,必须是一个__stdcall调用约定的函数,接受一个void*参数,并返回一个unsigned(代表退出码)。arglist: 传递给线程函数的参数。initflag: 创建标志,可以设置为0(立即运行)或CREATE_SUSPENDED(创建后挂起,需要手动调用ResumeThread)。thrdaddr: 指向一个unsigned变量的指针,用于接收线程 ID,可以为NULL。
- 返回值:
- 成功:返回一个可被
CloseHandle使用的有效句柄(类型为uintptr_t)。 - 失败:返回 0。
- 成功:返回一个可被
- 特点:
- 推荐使用。
- 线程函数必须是
__stdcall调用约定。 - 可以获取线程的退出码。
- 可以设置线程的安全属性。
- 可以控制线程的初始状态(挂起或运行)。
- 返回的句柄可以直接用于
WaitForSingleObject、CloseHandle等 Win32 API。
生命周期管理:_endthread 和 _endthreadex
当一个线程正常执行完毕时,它应该调用 _endthread 或 _endthreadex 来进行“清理工作”,这两个函数的作用是:

- 释放该线程的 TLS 数据。
- 调用
ExitThread来终止线程。
_endthread: 对应_beginthread,通常在线程函数的最后调用,但也可以在线程函数的任何地方调用以提前终止线程。_endthreadex: 对应_beginthreadex,它会将你传入的exit code作为参数传递给ExitThread,同样,通常在线程函数的最后调用。
重要提示:虽然在线程函数中手动调用 _endthread 或 _endthreadex是好的实践,但即使你不调用,当线程函数自然返回时,CRT 也会为你调用它们,显式调用可以确保在所有情况下都能正确清理,特别是当线程因错误需要提前退出时。
完整代码示例
下面是一个使用 _beginthreadex 的完整示例,它展示了创建线程、等待线程结束、获取返回码以及正确关闭句柄的完整流程。
#include <stdio.h>
#include <windows.h> // 用于 Sleep, WaitForSingleObject, GetExitCodeThread, CloseHandle
#include <process.h> // 用于 _beginthreadex
// 线程函数,必须是 __stdcall 调用约定
// 返回一个 unsigned 作为退出码
unsigned __stdcall ThreadFunction(void* pArguments) {
int* counter = (int*)pArguments;
printf("线程 %d 开始,\n", GetCurrentThreadId());
for (int i = 0; i < 5; i++) {
(*counter)++;
printf("线程 %d: 计数器 = %d\n", GetCurrentThreadId(), *counter);
Sleep(500); // 休眠 500 毫秒
}
printf("线程 %d 即将退出,返回码 42,\n", GetCurrentThreadId());
return 42; // 返回一个退出码
}
int main() {
HANDLE hThread = NULL;
unsigned threadID = 0;
int shared_counter = 0;
unsigned thread_exit_code = 0;
printf("主线程: 准备创建新线程,\n");
// 使用 _beginthreadex 创建线程
// 参数: 安全属性, 堆栈大小, 线程函数, 参数, 创建标志, 线程ID
hThread = (HANDLE)_beginthreadex(
NULL, // 默认安全属性
0, // 默认堆栈大小
ThreadFunction, // 线程函数
&shared_counter, // 传递给线程函数的参数
0, // 立即运行
&threadID // 用于接收线程ID
);
// 检查线程是否创建成功
if (hThread == 0) {
printf("错误:创建线程失败,错误码: %d\n", GetLastError());
return 1;
}
printf("主线程:线程创建成功,句柄 = %p, ID = %u\n", hThread, threadID);
// 主线程可以做自己的事情
for (int i = 0; i < 3; i++) {
printf("主线程: 正在忙...\n");
Sleep(300);
}
// 等待新线程执行完毕
// INFINITE 表示无限等待
WaitForSingleObject(hThread, INFINITE);
printf("主线程:子线程已结束,\n");
// 获取线程的退出码
GetExitCodeThread(hThread, (LPDWORD)&thread_exit_code);
printf("主线程:子线程的退出码是 %u\n", thread_exit_code);
// 关闭线程句柄,非常重要!
// 如果不关闭,会导致句柄泄漏
CloseHandle(hThread);
printf("主线程:程序结束,\n");
return 0;
}
总结与最佳实践
| 特性 | CreateThread (Win32 API) |
_beginthread (CRT) |
_beginthreadex (CRT) |
|---|---|---|---|
| 主要用途 | 创建内核级线程 | 创建线程并初始化 CRT 数据 | 创建线程并初始化 CRT 数据 |
| CRT 兼容性 | 不兼容,有内存泄漏风险 | 兼容 | 兼容 |
| 线程函数调用约定 | 任意 | __cdecl |
__stdcall |
| 获取退出码 | GetExitCodeThread |
不支持 | GetExitCodeThread |
| 线程句柄 | 可用 | 返回 uintptr_t,但非标准句柄 |
返回标准句柄,可用于所有 Win32 API |
| 创建标志 | CREATE_SUSPENDED 等 |
不支持 | 支持 CREATE_SUSPENDED |
| 推荐度 | 不推荐(除非你有特殊需求,如驱动开发) | 不推荐(功能太弱) | 强烈推荐 |
最佳实践:
- 总是优先使用
_beginthreadex:它在功能、灵活性和安全性上都优于_beginthread和CreateThread。 - 匹配调用约定:使用
_beginthreadex时,线程函数必须是__stdcall。 - 总是等待线程结束:在主线程中使用
WaitForSingleObject或WaitForMultipleObjects来等待工作线程完成,以确保所有操作都已完成。 - 总是关闭句柄:在线程不再需要时,使用
CloseHandle关闭_beginthreadex返回的句柄,以避免系统资源泄漏。 - 注意参数传递:线程函数的参数是
void*,你需要确保在线程内部正确地将其转换回原始类型,如果需要传递多个参数,最好将其封装到一个结构体中。
