Windows多线程C语言如何高效同步?

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

Windows 提供了一套功能强大且完整的 API 来创建和管理线程,我们将从最基础的 CreateThread 函数讲起,然后介绍更现代、更安全的 _beginthreadex 函数,并深入探讨线程同步的关键概念,如互斥锁、事件、信号量等。

c语言 多线程 windows
(图片来源网络,侵删)

目录

  1. Windows 线程 API 概览
    • CreateThread: 基础线程创建函数
    • _beginthreadex_endthreadex: C/C++ 运行时库的线程函数 (推荐)
  2. 一个简单的多线程示例
  3. 线程同步
    • 为什么需要同步? (竞态条件)
    • 互斥量
    • 事件
    • 信号量
    • 临界区 (轻量级互斥量)
  4. 线程通信
    • 全局变量
    • 结构体指针
  5. 完整示例:生产者-消费者模型
  6. 线程的生命周期与清理
  7. 最佳实践与注意事项

Windows 线程 API 概览

CreateThread (Windows API)

这是最底层的线程创建函数,由 Windows 内核提供。

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
);
  • 返回值: 成功返回一个 HANDLE (线程句柄),失败返回 NULL
  • lpThreadAttributes: 线程安全属性,通常传 NULL
  • dwStackSize: 线程栈大小,传 0 表示使用默认大小。
  • lpStartAddress: 线程的起始地址,是一个函数指针,线程创建后,从这个函数开始执行。
  • lpParameter: 传递给线程函数的参数,一个 void* 指针。
  • dwCreationFlags: 创建标志,传 0 表示立即创建并运行;传 CREATE_SUSPENDED 表示创建后挂起,需要用 ResumeThread 唤醒。
  • lpThreadId: 用于接收新线程的 ID,如果不需要可以传 NULL

重要警告: 在 C/C++ 程序中直接使用 CreateThread 可能会导致内存泄漏,因为 C 运行时库 的某些函数(如 malloc, fopen, strtok 等)内部使用静态数据来维护状态,当 CreateThread 创建的线程退出时,它不会自动清理这些静态数据,而主线程的 CRT 初始化代码也不会在新线程上运行,从而可能引发问题。

_beginthreadex_endthreadex (C Runtime Library - CRT)

为了解决 CreateThread 的问题,微软提供了 CRT 版本的线程函数,它们在内部会正确初始化和清理 CRT 环境。

// 创建线程
uintptr_t _beginthreadex(
  [in, optional]  void *security,
  [in]            unsigned stack_size,
  [in]            unsigned ( *start_address )( void * ),
  [in, optional]  void *arglist,
  [in]            unsigned initflag,
  [out]           unsigned *thrdaddr
);
// 结束线程 (在线程函数内部调用)
void _endthreadex(
  unsigned retval
);
  • 返回值: 成功返回一个 uintptr_t (可以转换为 HANDLE),失败返回 0
  • 参数与 CreateThread 类似,但函数签名和返回类型略有不同。
  • start_address: 是一个返回 unsigned 的函数指针。
  • _endthreadex: 当线程函数执行完毕后,应该调用 _endthreadex 来退出线程,而不是 return,这能确保 CRT 的资源被正确释放。

在 C/C++ 程序中,强烈推荐使用 _beginthreadex 而不是 CreateThread

c语言 多线程 windows
(图片来源网络,侵删)

一个简单的多线程示例

下面是一个使用 _beginthreadex 创建两个线程的简单例子,一个线程打印 "Hello from Thread 1",另一个打印 "Hello from Thread 2"。

#include <stdio.h>
#include <windows.h>
#include <process.h> // _beginthreadex 的头文件
// 线程函数的返回类型必须是 unsigned (__stdcall *)
// 参数必须是 void*
unsigned __stdcall ThreadFunc(void* pArg) {
    int threadNum = (int)(intptr_t)pArg; // 将 void* 转换回 int
    printf("Hello from Thread %d (ID: %u)\n", threadNum, GetCurrentThreadId());
    return 0; // 线程正常退出
}
int main() {
    HANDLE hThreads[2];
    unsigned threadIDs[2];
    // 创建第一个线程
    hThreads[0] = (HANDLE)_beginthreadex(NULL, 0, ThreadFunc, (void*)1, 0, &threadIDs[0]);
    if (hThreads[0] == 0) {
        printf("Failed to create thread 1. Error: %d\n", GetLastError());
        return 1;
    }
    // 创建第二个线程
    hThreads[1] = (HANDLE)_beginthreadex(NULL, 0, ThreadFunc, (void*)2, 0, &threadIDs[1]);
    if (hThreads[1] == 0) {
        printf("Failed to create thread 2. Error: %d\n", GetLastError());
        return 1;
    }
    // 等待所有线程执行完毕
    WaitForMultipleObjects(2, hThreads, TRUE, INFINITE);
    // 关闭线程句柄
    CloseHandle(hThreads[0]);
    CloseHandle(hThreads[1]);
    printf("All threads have finished.\n");
    return 0;
}

编译: 在 Visual Studio 中直接编译即可,如果使用 MinGW (GCC) 命令行编译,需要链接 pthread 库(尽管我们用的是 Windows API,但 MinGW 可能需要): gcc your_file.c -o your_program.exe -lws2_32 -lmsvcrt


线程同步

当多个线程访问共享资源(如全局变量、文件、硬件等)时,如果它们的执行顺序不确定,可能会导致数据不一致或程序崩溃,这就是竞态条件,同步机制用于控制线程的执行顺序,确保数据安全。

互斥量

互斥量就像一把钥匙,只有一个线程能拿到这把钥匙并进入“临界区”(访问共享资源的代码段),其他想进入的线程必须等待。

c语言 多线程 windows
(图片来源网络,侵删)
HANDLE hMutex; // 全局互斥量句柄
// 创建互斥量
hMutex = CreateMutex(NULL, FALSE, NULL); // 第二个参数 FALSE 表示初始状态为不拥有
// 在进入临界区前
WaitForSingleObject(hMutex, INFINITE); // 等待,直到获取到互斥量
// --- 临界区代码 ---
// 访问共享资源
// ---
// 离开临界区后
ReleaseMutex(hMutex); // 释放互斥量
// 使用完毕后关闭句柄
CloseHandle(hMutex);

事件

事件像一个信号灯,可以用来通知一个或多个某个事件已经发生。

  • 自动重置事件: 一个等待的线程被释放后,事件会自动变回无信号状态。
  • 手动重置事件: 所有等待的线程都会被释放,直到有人手动将其重置为无信号状态。
HANDLE hEvent; // 全局事件句柄
// 创建一个自动重置事件,初始状态为无信号
hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
// 线程A (等待者)
WaitForSingleObject(hEvent, INFINITE); // 会在这里阻塞,直到事件被触发
// 线程B (通知者)
// ... 执行一些操作 ...
SetEvent(hEvent); // 将事件设置为有信号,唤醒等待的线程
// 使用完毕后关闭句柄
CloseHandle(hEvent);

信号量

信号量像一个有固定数量停车位的停车场,它允许多个线程同时访问共享资源,但数量有限。

HANDLE hSemaphore; // 全局信号量句柄
// 创建一个信号量,初始资源数为2,最大资源数也为2
hSemaphore = CreateSemaphore(NULL, 2, 2, NULL);
// 线程A
WaitForSingleObject(hSemaphore, INFINITE); // 成功,资源数变为1
// ... 使用共享资源 ...
ReleaseSemaphore(hSemaphore, 1, NULL); // 释放,资源数变回2
// 线程B 和 C 也可以同时获取,但如果再有 D,D 就必须等待。

临界区

临界区是进程内的同步机制,比互斥量更快,因为它只在用户模式下工作,不涉及内核对象,但它不能跨进程使用。

CRITICAL_SECTION cs; // 全局临界区对象
// 初始化临界区
InitializeCriticalSection(&cs);
// 进入临界区
EnterCriticalSection(&cs);
// --- 临界区代码 ---
// 访问共享资源
// ---
// 离开临界区
LeaveCriticalSection(&cs);
// 删除临界区 (程序结束时)
DeleteCriticalSection(&cs);

线程通信

线程之间最常见的通信方式是共享内存,即访问同一个全局变量或动态分配的内存。

  • 传递简单数据: 通过 _beginthreadexlpParameter 参数传递。
  • 传递复杂数据: 定义一个结构体,将结构体的指针作为 lpParameter 传递。

示例:传递结构体

#include <stdio.h>
#include <windows.h>
#include <process.h>
typedef struct {
    int id;
    char* message;
} ThreadData;
unsigned __stdcall ThreadFunc(void* pArg) {
    ThreadData* data = (ThreadData*)pArg;
    printf("Thread %d: %s\n", data->id, data->message);
    free(data->message); // 释放动态分配的字符串
    free(data);          // 释放结构体内存
    return 0;
}
int main() {
    HANDLE hThread;
    unsigned threadID;
    ThreadData* data1 = (ThreadData*)malloc(sizeof(ThreadData));
    data1->id = 1;
    data1->message = _strdup("Hello from dynamic data!"); // _strdup 会分配内存
    hThread = (HANDLE)_beginthreadex(NULL, 0, ThreadFunc, data1, 0, &threadID);
    WaitForSingleObject(hThread, INFINITE);
    CloseHandle(hThread);
    return 0;
}

完整示例:生产者-消费者模型

这个经典模型展示了线程同步和通信的结合。

  • 共享资源: 一个固定大小的缓冲区。
  • 生产者线程: 不断向缓冲区放入数据。
  • 消费者线程: 不断从缓冲区取出数据。
  • 同步: 使用互斥量保护缓冲区,使用事件/信号量来通知缓冲区状态(空/满)。
#include <stdio.h>
#include <windows.h>
#include <process.h>
#define BUFFER_SIZE 5
int buffer[BUFFER_SIZE];
int in = 0, out = 0;
HANDLE hMutex, hEmptySlots, hFullSlots;
// 生产者
unsigned __stdcall Producer(void* pArg) {
    for (int i = 0; i < 10; ++i) {
        // 等待一个空槽位
        WaitForSingleObject(hEmptySlots, INFINITE);
        // 获取互斥锁以访问缓冲区
        WaitForSingleObject(hMutex, INFINITE);
        // --- 临界区:生产 ---
        buffer[in] = i;
        printf("Produced: %d at position %d\n", i, in);
        in = (in + 1) % BUFFER_SIZE;
        // ---
        // 释放互斥锁
        ReleaseMutex(hMutex);
        // 通知消费者,有一个新槽位被填满了
        ReleaseSemaphore(hFullSlots, 1, NULL);
    }
    return 0;
}
// 消费者
unsigned __stdcall Consumer(void* pArg) {
    for (int i = 0; i < 10; ++i) {
        // 等待一个满槽位
        WaitForSingleObject(hFullSlots, INFINITE);
        // 获取互斥锁以访问缓冲区
        WaitForSingleObject(hMutex, INFINITE);
        // --- 临界区:消费 ---
        int item = buffer[out];
        printf("Consumed: %d from position %d\n", item, out);
        out = (out + 1) % BUFFER_SIZE;
        // ---
        // 释放互斥锁
        ReleaseMutex(hMutex);
        // 通知生产者,有一个空槽位了
        ReleaseSemaphore(hEmptySlots, 1, NULL);
    }
    return 0;
}
int main() {
    HANDLE hThreads[2];
    // 创建同步对象
    hMutex = CreateMutex(NULL, FALSE, NULL);
    // 初始有 BUFFER_SIZE 个空槽位
    hEmptySlots = CreateSemaphore(NULL, BUFFER_SIZE, BUFFER_SIZE, NULL);
    // 初始有 0 个满槽位
    hFullSlots = CreateSemaphore(NULL, 0, BUFFER_SIZE, NULL);
    // 创建生产者和消费者线程
    hThreads[0] = (HANDLE)_beginthreadex(NULL, 0, Producer, NULL, 0, NULL);
    hThreads[1] = (HANDLE)_beginthreadex(NULL, 0, Consumer, NULL, 0, NULL);
    // 等待线程结束
    WaitForMultipleObjects(2, hThreads, TRUE, INFINITE);
    // 清理
    CloseHandle(hMutex);
    CloseHandle(hEmptySlots);
    CloseHandle(hFullSlots);
    CloseHandle(hThreads[0]);
    CloseHandle(hThreads[1]);
    printf("Producer-Consumer simulation finished.\n");
    return 0;
}

线程的生命周期与清理

  • 正常退出: 线程函数执行到 return 语句,或者调用 _endthreadex
  • 强制终止: 极其不推荐使用 TerminateThread(hThread, 0),这会导致线程立即停止,不会执行任何清理操作(如关闭文件、释放内存),可能导致资源泄漏和数据损坏,只有在万不得已且确定不会造成严重后果的情况下才使用。
  • 等待结束: 主线程应使用 WaitForSingleObjectWaitForMultipleObjects 来等待工作线程完成,这可以确保所有工作都已完成。
  • 关闭句柄: 线程结束后,其 HANDLE 变为“无效句柄”,为了释放系统资源,主线程应该调用 CloseHandle 来关闭这些句柄。

最佳实践与注意事项

  1. 总是使用 _beginthreadex: 避免使用 CreateThread 以防止 CRT 相关的问题。
  2. 最小化临界区: 尽量让临界区的代码块尽可能小,以减少线程阻塞的时间。
  3. 避免死锁: 当一个线程持有锁A并等待锁B,而另一个线程持有锁B并等待锁A时,就会发生死锁,一个简单的预防方法是按固定顺序获取锁
  4. 注意线程安全: 如果一个函数被多个线程调用,并且它依赖于静态或全局变量,那么它就不是线程安全的,要么避免在多线程环境中使用它,要么用锁来保护它。
  5. 小心参数传递: 传递给线程函数的参数(lpParameter)在线程运行期间必须保持有效,如果参数是在栈上分配的局部变量,在线程函数访问它之前,主线程可能已经返回,导致栈被破坏,最佳实践是使用 malloc 动态分配内存,并在线程函数内部 free 它。
  6. 考虑使用更高层次的库: 对于非常复杂的并发任务,可以考虑使用更高层次的库,如 C++11 的 <thread>, <mutex>, <condition_variable>,或者 Intel 的 Threading Building Blocks (TBB),它们提供了更现代、更安全的抽象。
-- 展开阅读全文 --
头像
dede免费模板适合摄影工作室用吗?
« 上一篇 01-13
dede批量替换文章内容怎么操作?
下一篇 » 01-13
取消
微信二维码
支付宝二维码

目录[+]