前言_beginthreadex是 Microsoft C 运行时CRT提供的线程创建函数声明在process.h中只能在 Windows MSVC或使用兼容 CRT 的 MinGW上使用。它不是 C 标准的一部分也不是 Win32 API 的一部分而是夹在两者之间的一层封装内部仍然调用CreateThread但在调用前后替 CRT 做了一些初始化工作。典型误解有两个。第一认为既然最终都调用CreateThread那直接用CreateThread就行_beginthreadex只是多此一举。实际上如果线程函数里用到 CRT 的设施errno、strtok、localtime、malloc等直接CreateThread可能让这些设施读到未初始化的每线程数据。第二认为_beginthreadex返回的句柄可以不管反正线程结束就自动释放。句柄是内核对象引用创建者必须CloseHandle否则会泄漏。本文讲清三件事CRT 为什么需要自己的线程入口、_beginthreadex的参数与返回值怎么用、它与std::thread的取舍。示例标注为 Windows 平台。本机没有编译环境代码只按公开文档的语义推演。一、为什么不直接调用 CreateThreadWin32 的CreateThread创建的是操作系统线程内核只负责调度栈和执行流它并不知道 C 运行时库的存在。而 CRT 的很多函数依赖“每线程状态”errno是一个典型例子标准要求它是线程局部的strtok的续接位置也是每线程的localtime返回的tm结构、rand的种子状态、浮点环境等同样如此。在 MSVC 的 CRT 实现里这些状态放在一个每线程的数据块中由 CRT 在“线程第一次调用 CRT 函数”时按需分配。_beginthreadex的包装逻辑就是先在堆上准备参数调用CreateThread在新线程真正进入用户函数前先建立 CRT 的每线程数据然后再调用用户提供的start_address用户函数返回后包装层调用_endthreadex清理并结束线程。如果绕过这层包装直接用CreateThread线程里第一次调用 CRT 时仍然可能触发按需初始化但这条路径在历史版本上并不可靠官方文档明确建议使用 CRT 的多线程程序应当用_beginthreadex/_beginthread而不是CreateThread。这不是风格问题而是运行时契约问题。_beginthread是更早的版本签名更简单但它有两个问题一是线程结束后会自动关闭句柄句柄值可能被系统复用导致等待或关闭时作用到错误的线程二是它不支持initflag和线程 ID 输出。因此现代代码若要用 CRT 线程入口应选_beginthreadex。二、函数签名与参数按 MSVC 文档_beginthreadex的原型是/* 适用于 C11_beginthreadex 是 MSVC CRT 扩展非标准 C */uintptr_t _beginthreadex(void* security,unsigned stack_size,unsigned (__stdcall* start_address)(void*),void* arglist,unsigned initflag,unsigned* thrdaddr);逐个说明参数含义常见取值security安全属性nullptr表示默认stack_size新线程栈大小0表示使用可执行文件头中的默认值start_address线程函数指针必须是__stdcall调用约定、参数void*、返回unsignedarglist传给线程函数的参数任意指针常指向堆上的结构体initflag初始状态0立即运行CREATE_SUSPENDED挂起创建thrdaddr输出线程 ID传unsigned变量的地址可为nullptr返回值类型是uintptr_t成功时是线程句柄的整数值失败时返回0。由于句柄在 Win32 中是HANDLE即void*使用时需要reinterpret_castHANDLE。失败时 CRT 还会设置errno可以用errno或_get_errno查看原因。线程函数的签名必须是unsigned __stdcall f(void*)。__stdcall在 x64 上被忽略只有一种调用约定但在 x86 上不能省否则栈平衡会出错。返回值会作为线程的退出码用GetExitCodeThread可以取到。三、完整的创建与回收流程下面是一个完整示例。它演示了参数传递、等待、关闭句柄三个必要步骤。// 适用于 C17Windows 平台MSVC CRT#define WIN32_LEAN_AND_MEAN#include windows.h#include process.h#include cstdint#include cerrno#include iostream#include stringstruct ThreadArg {int id;std::string name;};// 必须匹配 unsigned (__stdcall*)(void*)unsigned __stdcall worker(void* raw){auto* arg static_castThreadArg*(raw);std::cout worker arg-id name arg-name \n;// 返回即等价于调用 _endthreadex(返回值)return 0;}int main(){ThreadArg arg{7, demo};unsigned tid 0;uintptr_t raw _beginthreadex(nullptr, // 默认安全属性0, // 默认栈大小worker, // 线程函数arg, // 参数地址0, // 立即运行tid); // 输出线程 IDif (raw 0) {std::cerr create thread failed, errno errno \n;return 1;}HANDLE h reinterpret_castHANDLE(raw);// 等待线程结束再关闭句柄WaitForSingleObject(h, INFINITE);CloseHandle(h);return 0;}几个细节值得展开。第一arg的生命周期必须覆盖线程的执行期。上例中main用WaitForSingleObject等待所以arg在线程运行时一直有效。如果把局部变量地址交给线程后立刻离开作用域线程拿到的就是悬垂指针这是未定义行为。第二句柄的所有权在创建者。_beginthreadex不会替你关闭句柄必须CloseHandle。关闭句柄不会终止线程只是减少内核对象引用计数线程仍在运行也没关系等它结束后对象自然销毁。第三不要在主线程调用_endthreadex。它设计为“结束当前线程”从别的线程调用属于误用主线程正常return即可。第四如果需要在线程函数中途提前退出可以调用_endthreadex(code)它会执行 CRT 的清理不要用ExitThread那会跳过 CRT 清理。也不要在 C 代码里用TerminateThread它不展开栈、不运行析构函数几乎必然造成资源泄漏。四、与 std::thread 的取舍C11 起有了std::thread同样能在 Windows 上创建线程并且在 POSIX 平台也能用。它的可移植性远好于_beginthreadex正常情况下应当优先选择。// 适用于 C17#include iostream#include string#include threadint main(){std::string name demo;std::thread t([name] {std::cout worker name name \n;});t.join(); // 析构前必须 join 或 detachreturn 0;}维度_beginthreadexstd::thread可移植性仅 Windows跨平台标准CRT 扩展C11 标准参数传递void*需自行打包可变参数自动转发返回值句柄 线程 IDstd::thread对象回收CloseHandle 等待join()或detach()异常线程函数不能抛异常穿越 C 边界未捕获异常调用std::terminate适用场景维护老代码、需要精细控制新代码的默认选择需要强调的是std::thread在 Windows 上的实现通常也建立在 CRT 线程入口之上因此它同样满足 CRT 的每线程数据要求。只有在维护既有代码、需要显式设置CREATE_SUSPENDED、或者需要拿到原生句柄做WaitForMultipleObjects时才有理由直接使用_beginthreadex。常见坑点❌ 把线程函数写成void* worker(void*)或unsigned worker(void*)。✅ 必须是unsigned __stdcall worker(void*)。x86 上调用约定不匹配会破坏栈。❌ 把栈上局部变量的地址交给线程后立即返回。✅ 线程参数的生命周期要覆盖线程执行期用join等待或把参数放到堆上并由线程负责释放。❌ 创建成功后不调用CloseHandle。✅ 每个成功返回的句柄都要关闭否则内核对象引用计数不归零。❌ 认为CloseHandle会终止线程。✅ 它只释放句柄引用线程照常运行。❌ 用ExitThread或TerminateThread结束线程。✅ 线程函数正常return或调用_endthreadexTerminateThread不展开栈析构函数不会运行。❌ 忽略_beginthreadex的返回值是uintptr_t而不是HANDLE。✅ 比较时对0判断使用时reinterpret_castHANDLE不要直接赋给HANDLE变量而不转换。❌ 在线程函数里抛出 C 异常指望主线程捕获。✅ 异常不能穿越 C 函数边界线程函数应在内部捕获所有异常并转换为退出码。❌ 在有更现代选择的跨平台项目里坚持用_beginthreadex。✅ 新代码优先std::thread仅在与 Win32 句柄深度集成或维护老代码时使用 CRT 入口。总结要点结论归属Windows CRT 扩展声明于process.h与 CreateThread 的关系内部调用它额外初始化 CRT 每线程数据返回值uintptr_t0 表示失败需转HANDLE线程函数unsigned __stdcall f(void*)资源回收创建者CloseHandle并等待线程结束现代替代std::thread可移植且类型安全禁止操作TerminateThread、跨线程_endthreadex_beginthreadex的价值不在“创建线程”这个动作本身而在于它替 CRT 补上了每线程状态的初始化。理解这一层才能明白为什么同样的代码用CreateThread偶尔会出问题也才能在维护老项目时判断哪些调用可以安全替换为std::thread。