c语言 beginthread

99ANYc3cd6
预计阅读时长 20 分钟
位置: 首页 C语言 正文

为什么需要 _beginthread 而不是 CreateThread

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

c语言 beginthread
(图片来源网络,侵删)
  • errno:错误码。
  • strerror:错误信息缓存。
  • 文件句柄表。
  • 内存分配器(malloc)的状态。

当你使用 Windows API 的 CreateThread 函数创建一个新线程时,这个新线程并不会自动初始化这些 CRT 所需的 TLS 数据,它只会继承调用线程的堆栈,但不会继承 CRT 的“上下文”。

这会导致什么问题?

如果在由 CreateThread 创建的线程中调用了任何 CRT 函数(这几乎是不可避免的),printfmallocfopen,线程可能会访问到未初始化或错误的 TLS 数据,从而引发不可预测的行为,最常见的后果就是:

  • 内存泄漏mallocfree 的内部状态混乱,导致内存无法正确释放。
  • 程序崩溃:访问无效的 TLS 数据导致访问冲突。
  • 数据损坏errno 或其他状态被错误地修改。

_beginthread_beginthreadex 的作用

_beginthread_beginthreadex 就是为了解决这个问题而设计的,它们是 CRT 提供的函数,在创建新线程的同时,会自动初始化该线程所需的所有 CRT TLS 数据

c语言 beginthread
(图片来源网络,侵删)

它们是 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 调用约定。
    • 可以获取线程的退出码。
    • 可以设置线程的安全属性。
    • 可以控制线程的初始状态(挂起或运行)。
    • 返回的句柄可以直接用于 WaitForSingleObjectCloseHandle 等 Win32 API。

生命周期管理:_endthread_endthreadex

当一个线程正常执行完毕时,它应该调用 _endthread_endthreadex 来进行“清理工作”,这两个函数的作用是:

c语言 beginthread
(图片来源网络,侵删)
  1. 释放该线程的 TLS 数据。
  2. 调用 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
推荐度 不推荐(除非你有特殊需求,如驱动开发) 不推荐(功能太弱) 强烈推荐

最佳实践:

  1. 总是优先使用 _beginthreadex:它在功能、灵活性和安全性上都优于 _beginthreadCreateThread
  2. 匹配调用约定:使用 _beginthreadex 时,线程函数必须是 __stdcall
  3. 总是等待线程结束:在主线程中使用 WaitForSingleObjectWaitForMultipleObjects 来等待工作线程完成,以确保所有操作都已完成。
  4. 总是关闭句柄:在线程不再需要时,使用 CloseHandle 关闭 _beginthreadex 返回的句柄,以避免系统资源泄漏。
  5. 注意参数传递:线程函数的参数是 void*,你需要确保在线程内部正确地将其转换回原始类型,如果需要传递多个参数,最好将其封装到一个结构体中。
-- 展开阅读全文 --
头像
c语言 sublimetext
« 上一篇 01-04
dede如何用php判断当前栏目id?
下一篇 » 01-04

相关文章

取消
微信二维码
支付宝二维码

目录[+]