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

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

Windows 提供了一套强大的原生多线程 API,主要在 <windows.h> 头文件中定义,这套 API 功能全面,但相对底层,学习曲线稍陡,下面我将从核心概念、关键 API、同步机制到完整示例,一步步为你讲解。

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

核心概念与头文件

在开始之前,你需要了解几个核心概念:

  • 线程: 程序执行的最小单位,一个进程可以拥有多个线程,它们共享进程的内存空间和资源。
  • 句柄: Windows 中用来标识一个资源(如窗口、文件、线程、进程等)的整数,对于线程,我们使用 HANDLE 类型。
  • ID: 一个唯一的标识符,用来区分不同的线程,类型为 DWORD
  • 同步: 当多个线程访问共享资源时,需要一种机制来协调它们的执行顺序,以避免数据竞争和不一致,这是多线程编程中最重要也最复杂的问题。

必需的头文件:

#include <windows.h> // Windows API 的核心头文件
#include <stdio.h>   // 用于标准输入输出

创建线程

创建新线程使用 CreateThread 函数,这是最核心的函数。

CreateThread 函数原型

HANDLE CreateThread(
  LPSECURITY_ATTRIBUTES   lpThreadAttributes, // 安全属性,通常传 NULL
  SIZE_T                  dwStackSize,        // 栈大小,0 表示使用默认大小
  LPTHREAD_START_ROUTINE  lpStartAddress,     // 线程函数的地址
  LPVOID                  lpParameter,        // 传递给线程函数的参数
  DWORD                   dwCreationFlags,    // 创建标志,0 表示立即运行
  LPDWORD                 lpThreadId          // 用于接收线程ID的指针,可传 NULL
);
  • lpStartAddress: 这是最重要的参数,它是一个函数指针,指向你的线程函数,这个函数必须遵循特定的签名:

    c语言 windows多线程
    (图片来源网络,侵删)
    DWORD WINAPI ThreadFunction(LPVOID lpParam);
    • WINAPI: 是 __stdcall 的宏定义,规定了函数调用时的参数传递方式。
    • LPVOID: 指向任意类型的指针,用于传递参数。
    • 返回值类型为 DWORD,可以看作是线程的“退出码”。
  • lpParameter: 你想传递给线程函数的任何数据,如果不需要传递数据,可以设为 NULL

  • 返回值:

    • 成功:返回一个新线程的 HANDLE
    • 失败:返回 NULL,你需要调用 GetLastError() 来获取具体的错误码。

线程函数与参数

线程函数是线程的“主心骨”,它包含了线程要执行的所有逻辑。

示例:一个简单的线程函数

c语言 windows多线程
(图片来源网络,侵删)
// 线程函数
DWORD WINAPI MyThreadFunction(LPVOID lpParam) {
    // 将 lpParam 转换为实际的数据类型
    int threadNumber = (int)(DWORD_PTR)lpParam; // 使用 (DWORD_PTR) 避免编译器警告
    for (int i = 0; i < 5; i++) {
        printf("Hello from thread %d! Count: %d\n", threadNumber, i);
        Sleep(500); // 休眠 500 毫秒
    }
    printf("Thread %d is finished.\n", threadNumber);
    return 0; // 返回退出码
}

注意: lpParam 是一个 32 位或 64 位的指针,在 64 位系统上,直接将它强制转换为 int 可能会丢失高位信息,推荐使用 (DWORD_PTR)INT_PTR 作为中间类型进行转换,这是 Windows API 推荐的做法。


等待与结束线程

主线程需要一种方式来等待子线程完成,或者在子线程结束后清理资源。

WaitForSingleObject

这是一个最基本的同步函数,可以让主线程“阻塞”,直到指定的对象(在这里是线程)变为“已 signaled”状态。

  • 对于线程,当线程执行完毕后,它的状态就会变为 signaled。
  • INFINITE 表示无限期等待。
HANDLE hThread = CreateThread(...);
if (hThread == NULL) {
    // 错误处理
}
// 主线程在这里等待,直到 hThread 代表的线程执行完毕
WaitForSingleObject(hThread, INFINITE);
// 线程结束后,关闭其句柄以释放资源
CloseHandle(hThread);

ExitThreadTerminateThread

  • ExitThread(DWORD dwExitCode): 线程主动调用此函数来结束自己。不推荐使用,因为它不会执行 C/C++ 运行时库的清理工作(如全局对象的析构函数)。
  • TerminateThread(HANDLE hThread, DWORD dwExitCode): 强制结束一个线程。极度不推荐使用!这会导致线程立即停止,无法释放其拥有的资源(如锁、内存等),极易造成死锁和数据损坏,只在极端紧急情况下考虑。

最佳实践:让线程函数自然执行完毕并返回。


线程同步(核心重点)

当多个线程读写同一块共享数据时,就会发生“数据竞争”,导致不可预测的结果,Windows 提供了多种同步对象来解决这个问题。

1 临界区

临界区是最轻量级的同步机制,它只用于同一进程内的线程同步。

  • 特点:速度快,但不能跨进程。
  • 原理:同一时间只允许一个线程进入临界区。

关键函数:

  • InitializeCriticalSection(&CriticalSection): 初始化临界区。
  • EnterCriticalSection(&CriticalSection): 进入临界区,如果另一个线程已经进入,则当前线程会在此等待,直到临界区被释放。
  • LeaveCriticalSection(&CriticalSection): 离开临界区,允许其他线程进入。
  • DeleteCriticalSection(&CriticalSection): 删除临界区(在线程不再使用后)。

2 互斥量

互斥量与临界区功能类似,但它是一个内核对象,可以用于跨进程的线程同步。

  • 特点:比临界区慢,但功能更强大。
  • 原理:与临界区类似,但可以被操作系统调度。

关键函数:

  • CreateMutex(NULL, FALSE, NULL): 创建一个互斥量。
  • WaitForSingleObject(hMutex, INFINITE): 等待并获取互斥量所有权。
  • ReleaseMutex(hMutex): 释放互斥量所有权。
  • CloseHandle(hMutex): 关闭互斥量句柄。

3 事件

事件是一个内核同步对象,用于线程间的通知。

  • 特点:一个线程可以设置事件,另一个或多个线程可以等待事件被设置。
  • 类型:
    • 自动重置事件: 当一个等待线程被释放后,事件会自动自动变回非 signaled 状态。
    • 手动重置事件: 当一个等待线程被释放后,事件会保持 signaled 状态,直到有人手动将其重置。

关键函数:

  • CreateEvent(NULL, FALSE, FALSE, NULL): 创建一个自动重置、初始状态为 nonsignaled 的事件。
  • SetEvent(hEvent): 将事件设置为 signaled 状态。
  • ResetEvent(hEvent): 将事件重置为 nonsignaled 状态。
  • WaitForSingleObject(hEvent, INFINITE): 等待事件变为 signaled。

4 信号量

信号量用于控制对有限数量资源的访问。

  • 特点:它维护一个计数器。WaitForSingleObject 会使计数器减一,ReleaseSemaphore 会使计数器加一,只有当计数器大于0时,等待才能成功。
  • 用途:控制一个最多有5个线程的线程池。

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

这个例子综合了线程创建、参数传递、临界区同步等知识点。

场景:一个“生产者”线程向一个共享队列添加数据,一个“消费者”线程从队列中取出数据,为了安全访问队列,我们使用临界区。

#include <windows.h>
#include <stdio.h>
#define BUFFER_SIZE 5
// 共享数据结构
typedef struct {
    int buffer[BUFFER_SIZE];
    int in;   // 生产者写入位置
    int out;  // 消费者读取位置
    CRITICAL_SECTION cs; // 临界区,用于保护共享数据
} SharedData;
// 生产者线程函数
DWORD WINAPI ProducerThread(LPVOID lpParam) {
    SharedData* data = (SharedData*)lpParam;
    for (int i = 0; i < 10; i++) {
        // 模拟生产数据
        int item = i * 100;
        // 进入临界区
        EnterCriticalSection(&data->cs);
        // 检查队列是否已满(简单示例,未做循环队列处理)
        if (data->in < BUFFER_SIZE) {
            data->buffer[data->in] = item;
            printf("Producer: Produced item %d at position %d\n", item, data->in);
            data->in++;
        } else {
            printf("Producer: Buffer is full!\n");
        }
        // 离开临界区
        LeaveCriticalSection(&data->cs);
        Sleep(100); // 模拟生产耗时
    }
    return 0;
}
// 消费者线程函数
DWORD WINAPI ConsumerThread(LPVOID lpParam) {
    SharedData* data = (SharedData*)lpParam;
    int item;
    for (int i = 0; i < 10; i++) {
        // 进入临界区
        EnterCriticalSection(&data->cs);
        // 检查队列是否为空
        if (data->out < data->in) {
            item = data->buffer[data->out];
            printf("Consumer: Consumed item %d from position %d\n", item, data->out);
            data->out++;
        } else {
            printf("Consumer: Buffer is empty!\n");
        }
        // 离开临界区
        LeaveCriticalSection(&data->cs);
        Sleep(200); // 模拟消费耗时
    }
    return 0;
}
int main() {
    HANDLE hProducer, hConsumer;
    SharedData data = { {0}, 0, 0 };
    // 初始化临界区
    InitializeCriticalSection(&data.cs);
    // 创建生产者线程
    hProducer = CreateThread(NULL, 0, ProducerThread, &data, 0, NULL);
    if (hProducer == NULL) {
        printf("Failed to create producer thread.\n");
        return 1;
    }
    // 创建消费者线程
    hConsumer = CreateThread(NULL, 0, ConsumerThread, &data, 0, NULL);
    if (hConsumer == NULL) {
        printf("Failed to create consumer thread.\n);
        CloseHandle(hProducer);
        return 1;
    }
    // 等待两个线程都执行完毕
    WaitForSingleObject(hProducer, INFINITE);
    WaitForSingleObject(hConsumer, INFINITE);
    // 清理资源
    CloseHandle(hProducer);
    CloseHandle(hConsumer);
    DeleteCriticalSection(&data.cs);
    printf("All threads finished.\n");
    return 0;
}

现代 C++ 的替代方案 (std::thread)

如果你使用的是支持 C++11 或更高版本的编译器,强烈推荐使用 std::thread,而不是原生的 Windows API。std::thread 是跨平台的,代码更简洁、更安全、更易于维护。

使用 std::thread 重写上面的生产者-消费者示例:

#include <iostream>
#include <thread>
#include <vector>
#include <mutex>
#include <queue>
std::mutex mtx; // C++ 的互斥量,对应 Windows 的临界区
std::queue<int> data_queue;
void producer() {
    for (int i = 0; i < 10; ++i) {
        int item = i * 100;
        {
            std::lock_guard<std::mutex> lock(mtx); // 自动加锁,作用域结束自动解锁
            data_queue.push(item);
            std::cout << "Producer: Produced item " << item << std::endl;
        }
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
}
void consumer() {
    for (int i = 0; i < 10; ++i) {
        int item = -1;
        {
            std::lock_guard<std::mutex> lock(mtx);
            if (!data_queue.empty()) {
                item = data_queue.front();
                data_queue.pop();
                std::cout << "Consumer: Consumed item " << item << std::endl;
            } else {
                std::cout << "Consumer: Buffer is empty!" << std::endl;
            }
        }
        std::this_thread::sleep_for(std::chrono::milliseconds(200));
    }
}
int main() {
    std::thread t1(producer);
    std::thread t2(consumer);
    t1.join();
    t2.join();
    std::cout << "All threads finished." << std::endl;
    return 0;
}

对比:可以看到,std::thread + std::mutex 的代码更短,可读性更高,并且没有 CloseHandleDeleteCriticalSection 等繁琐的清理工作。

特性 Windows API (<windows.h>) C++11 std::thread
平台 仅 Windows 跨平台
易用性 较低,API 较多,需要手动管理句柄 高,语法简洁,RAII 管理资源
性能 原生性能高 通常有良好优化,性能接近原生
同步 临界区、互斥量、事件、信号量等 std::mutex, std::condition_variable
推荐场景 纯 C 语言项目。
需要调用特定 Windows 线程 API。
对性能有极致要求且需要精细控制。
C++ 项目(强烈推荐)。
需要代码可移植性。
追求开发效率和代码安全。

对于新的 C++ 项目,请优先选择 std::thread,对于纯 C 语言项目或在特定 Windows 环境下,掌握 Windows 原生线程 API 是一项非常重要的技能。

-- 展开阅读全文 --
头像
dede首页列表如何调用图片?
« 上一篇 02-16
织梦后台用户名不存在怎么办?
下一篇 » 02-16
取消
微信二维码
支付宝二维码

目录[+]