揭秘!为何要用_beginthreadex,而不用CreateThread和_beginthread

本文解释了在C/C++中使用_beginthreadex而非CreateThread的原因。详细介绍了_beginthreadex如何确保线程安全,以及与_threadstartex和_endthreadex的交互过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

       由于历史原因,所以C/C++运行库并不是为多线程应用程序而设计的,所以为了保证其中的某些变量和函数的安全,那么必须创建一个数据结构,并使之与使用了C/C++运行库函数的每个线程所关联。当在调用C/C++运行库函数时,那些函数必须读取主调自己的线程的数据块,从而避免印象其他线程。

       所以当编写C/C++代码时,请调用_beginthreadex,而不要使用CreateThread的大致原因就是如上所述。详细内容请继续往下读😀😺😃

       _beginthreadex的函数参数列表与CreateThread一样,但是参数名和类型有所不同。这是因为C/C++程序不应该对Windows系统的类型有任何的依赖。

       _beginthreadex也会返回新建线程的句柄,就像CreateThread一样。可以非常方便的替换自己程序中的CreateThread函数,但是数据类型不一样,所以还得添加一些类型转换即可。

       透过_beginthreadex函数的源码我们可知,

        1.每个线程都有自己专用的_tiddata内存块,它们是从C/C++运行库的堆上分配的。

        2.传给_beginthreadex的线程函数地址(pfnStartAddr)保存在_tiddata内存块中。(_tiddata结构可以在Mtdll.h源码中窥探一二)

        3._beginthreadex会在内部调用CreateThread。

       4.调用CreateThread时传入的地址是_threadstartex(而非传入的pfnStartAddr)。参数地址是_tiddata结构地址,而非pvParam。

        5.如果一切顺利,会返回线程的句柄,操作失败会返回0。

 

      初始化并且传入_tiddata后,又怎么将其和线程关联呢?继续往下看😀😺😃

      对于_threadstartex函数及其辅助函数__callthreadstartex:

1.新的线程首先执行RtlUserThreadStart,然后在跳转到_threadstartex。(这便于上一篇所讲的"创建线程的内幕"一文中的知识点所契合)

2._threadstartex唯一的参数就是新线程的_tiddata内存块地址。

3.TlsSetValue是一个操作系统函数,作用是将一个值与主调函数关联起来。(这就是所谓的线程局部存储TLS)_threadstartex函数将_tiddata内存块与新建线程关联起来。------华丽的分割线--------

        4.无参数的辅助函数__callthreadstartex中,有一个SEH帧,它处理着许多与运行库有关的事情——比如运行时错误(如抛出未被捕获的C++异常)——和c/c++运行库的signal函数。这点很重要,因为用CreateThread创建的线程,调用C/C++运行库的signal函数,其是不能正常工作的。

       5.预期要执行的线程函数会被调用,并向其传递预期的参数。(线程的地址和参数都是存在于TLS中保存的_tiddata数据块中,并会在__callthreadstartex中在TLS中获取得到)

6.线程函数的返回值被认为是线程的退出代码。注意:函数不是返回_threadstartex然后返回RltUserThreadStart(因为这样_tiddata内存块不会被释放),而是调用_endthreadex,并向其传入线程函数的返回值。

对于_endthreadex,其内部先获取TLS中的_tiddata块,然后释放掉,并调用操作系统的ExitThread函数来实际销毁线程,并传递正确的退出码。

 

        总结:所以现在应该理解为什么不要调用CreateThread了,因为调用CreateThread后,在线程调用一个需要_tiddata块的c/c++运行库函数时(主要通过TlsGetValue),会得到NULL,这时c/c++运行库会为其初始化一个块,然后这个块与线程关联(主要通过TlsSetValue)。这之后,任何使用_tiddata块的函数都能正常使用了。但是,问题还是有的:1.如果线程使用了signal函数,则整个进程会终止掉(上文有详细描述,关于__callthreadstartex的SEH帧没有准备就绪)。2.线程入口点函数返回后,会在RtlUserThreadStart中直接调用ExitThread,其并没有释放掉_tiddata内存块,这会引起内存泄漏。

 

 

        附加:绝不调用_beginthread。因为,

1._beginthread参数较少,不能创建具有安全属性的线程,不能让线程立即挂起等。_endthread也是如此,其退出代码被硬编码为0。

_endthread还存在一个问题,就是在调用ExitThread前,会调用CloseHandle,向其传入新线程的句柄。举个例子,比如你使用了_beginthread创建了一个新的线程,在其之后使用CloseHandle关闭线程句柄。由于_beginthread会调用_endthread函数终止线程,而线程内核对象的初始化计数为2,在_endthread函数中,调用ExitThread前,会调用CloseHandle,也就是说会连续两次递减引用计数,这时引用计数为0,内核对象被系统销毁,再之后在调用CloseHandle关闭线程句柄时,这个传入的句柄是无效的,即CloseHandle函数的调用会以失败告终。

所以请用_beginthreadex代替_beginthread,_endthreadex代替_endthread。      _beginthreadex调用的是_endthreadex,_beginthread调用的是_endthread

 

 

 

<think>我们正在讨论WindowsAPI中CreateThread_beginthreadex在内存管理方面的本质区别。关键点在于C/C++运行时库(CRT)的初始化。1.CreateThread是WindowsAPI函数,直接创建操作系统线程,但不会初始化CRT的线程局部存储(TLS)数据。这可能导致:-使用CRT函数(如malloc,printf)时,如果这些函数依赖于TLS(例如errno),会出现未定义行为。-在线程退出时,CRT分配的内存(如线程局部变量)可能不会被正确释放,造成内存泄漏。2._beginthreadex是CRT提供的函数,它在调用CreateThread之前会:-为线程分配并初始化一个tiddata结构(存储errno、strtok上下文等线程特定数据)。-将tiddata与线程关联(通过TLS)。-确保线程退出时通过_endthreadex正确释放tiddata。因此,在C/C++程序中,应使用_beginthreadex来避免CRT相关的内存问题。引用用户提供的错误信息:用户在使用AfxBeginThread时遇到重载匹配错误,这实际上是因为MFC的AfxBeginThread有两个重载版本,用户传递的函数类型与第一个重载(接收AFX_THREADPROC类型)匹配,但AFX_THREADPROC定义为UINT(__cdecl*AFX_THREADPROC)(LPVOID),而用户函数声明为UINTClientThread(LPVOIDpParam),默认调用约定为__stdcall(在WindowsAPI中常见)。因此需要显式指定调用约定:UINT__cdeclClientThread(LPVOIDpParam);但这个问题与当前问题无关,当前问题是关于CreateThread_beginthreadex的内存管理区别。现在,我们详细解释内存管理区别:假设有以下代码:使用CreateThread:DWORDWINAPIThreadFunc(LPVOID){printf("Hello");//可能崩溃或内存泄漏,因为CRT状态未初始化return0;}CreateThread(NULL,0,ThreadFunc,NULL,0,NULL);使用_beginthreadex:unsigned__stdcallThreadFunc(void*){printf("Hello");//安全,因为CRT状态已初始化return0;}_beginthreadex(NULL,0,ThreadFunc,NULL,0,NULL);总结:-CreateThread:操作系统层级线程创建,不涉及CRT,因此需要确保不依赖CRT功能(或手动初始化)。-_beginthreadex:封装了CreateThread,并添加了CRT初始化/清理,是C/C++程序的安全选择。注意:在C++11及以上,应使用std::thread,它内部会处理这些细节。参考文献:[^1]:Microsoft文档明确指出:在由_createthread、_beginthread或_beginthreadex创建的线程中,可以使用CRT函数;而由CreateThread创建的线程中,除非采取特殊步骤(如显式初始化CRT的TLS),否则不应使用CRT函数。相关问题:§§相关问题§§1.为什么在C++程序中直接使用CreateThread调用printf可能会导致问题?2.如何手动初始化CRT状态以便在CreateThread创建的线程中使用CRT函数?3.在MFC程序中,AfxBeginThread内部是如何管理线程内存的?4.std::thread与_beginthreadex在内存管理上有何异同?</think>### CreateThread vs _beginthreadex 内存管理本质区别 在 Windows API 中,`CreateThread` `_beginthreadex` 的核心区别在于 **C/C++运行时库(CRT)的线程局部存储(TLS)初始化**。以下是本质差异的对比分析: | **特性** | **CreateThread** | **_beginthreadex** | |------------------------|---------------------------------------------|---------------------------------------------| | **所属库** | Windows API (kernel32.dll) | C/C++运行时库 (msvcrt.dll) | | **内存泄漏风险** | 高:未初始化CRT的TLS结构 | 低:显式分配/释放TLS结构[^1] | | **线程局部变量** | 不支持`errno`、`strtok`等CRT线程局部变量 | 完全支持CRT线程局部变量 | | **清理机制** | 无CRT资源清理 | 自动调用`_endthreadex`释放TLS内存 | | **函数签名** | `DWORD WINAPI ThreadProc(LPVOID lpParam)` | `unsigned __stdcall ThreadProc(void*)` | #### 关键区别详解: 1. **TLS初始化机制** - `CreateThread`: 仅创建操作系统原生线程,**不初始化**CRT的线程局部存储(如`_tiddata`结构)。 若线程中使用`malloc`/`printf`等CRT函数,会导致: - 未初始化的`errno`变量 - `strtok`等函数状态冲突 - 潜在的**内存泄漏**(约72字节/线程)[^1] - `_beginthreadex`: 内部调用`CreateThread`,但额外: ```c _ptiddata ptd = _calloc_crt(1, sizeof(struct _tiddata)); // 分配TLS结构 _initptd(ptd); // 初始化CRT状态 _callthreadentry(...); // 关联线程入口 ``` 2. **资源清理对比** - `CreateThread`线程退出时: CRT分配的TLS结构**不会被释放** → 内存泄漏。 - `_beginthreadex`线程退出时: 自动触发`_endthreadex` → 释放`_tiddata`内存: ```c void __cdecl _endthreadex(unsigned retcode) { _ptiddata ptd = _getptd(); // 获取TLS结构 _freeptd(ptd); // 释放内存 ExitThread(retcode); // 调用系统API } ``` 3. **实际影响案例** 若在`CreateThread`创建的线程中使用CRT函数: ```c DWORD WINAPI MyThread(LPVOID) { printf("Hello"); // 可能访问无效的TLS结构 return 0; // 退出时72字节泄漏[^1] } ``` 而使用`_beginthreadex`: ```c unsigned __stdcall MyThread(void*) { printf("Hello"); // 安全的TLS访问 return 0; // 内存自动释放 } ``` #### 最佳实践: 1. **C/C++程序**必须使用`_beginthreadex` - 避免CRT相关内存泄漏 - 确保线程局部变量正常工作 2. **纯Win32 API程序**(无CRT)可用`CreateThread` - 例如:仅调用`WriteFile`/`ReadFile`的线程 > **根本原因**:CRT库(如`msvcrt.dll`)依赖`_tiddata`结构存储线程状态。`CreateThread`绕过该初始化,而`_beginthreadex`是专为CRT设计的线程安全封装。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值