windows c语言多线程编程

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

Windows 提供了一套强大的多线程 API,主要通过 windows.h 头文件来使用,下面我将从核心概念、关键 API、同步机制、完整示例以及最佳实践等方面进行详细说明。

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

核心概念

在开始之前,理解几个基本概念很重要:

  • 进程:一个正在运行的程序,它拥有独立的内存空间和系统资源。
  • 线程:进程内的一个执行流,一个进程可以包含一个或多个线程,它们共享该进程的资源(如内存、文件句柄等),但每个线程有自己的调用栈和程序计数器。
  • 并发:宏观上,多个任务看起来在同时运行,在单核 CPU 上,操作系统通过快速切换线程来实现“执行的假象。
  • 并行:微观上,多个任务确实在同时运行,在多核 CPU 上,不同的线程可以被分配到不同的核心上同时执行。

关键 API 和数据结构

所有 Windows 多线程相关的函数和类型都定义在 windows.h 中。

1 创建线程

创建新线程使用 CreateThread 函数。

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
);

参数详解:

windows c语言多线程编程
(图片来源网络,侵删)
  • lpThreadAttributes: 指向 SECURITY_ATTRIBUTES 结构的指针,通常设为 NULL,表示使用默认安全属性。
  • dwStackSize: 线程的初始栈大小,以字节为单位,如果为 0,则使用默认大小(通常是 1MB)。
  • lpStartAddress: 线程的入口函数地址,这是一个函数指针,类型为 LPTHREAD_START_ROUTINE,线程创建后,会立即从这个函数开始执行。
  • lpParameter: 传递给线程入口函数的参数,这是一个 void* 类型的指针,你可以用它来传递任何数据结构(需要将结构体地址强制转换)。
  • dwCreationFlags: 线程的创建标志。
    • 0: 线程创建后立即开始运行。
    • CREATE_SUSPENDED: 线程创建后处于挂起状态,直到调用 ResumeThread 才会运行。
  • lpThreadId: 指向一个 DWORD 变量的指针,用于接收新线程的 ID,如果不需要,可以设为 NULL

返回值:

  • 成功:返回一个 HANDLE 类型的句柄,代表新创建的线程。
  • 失败:返回 NULL,可以通过调用 GetLastError() 获取具体的错误代码。

2 线程入口函数

线程入口函数的原型是固定的:

DWORD WINAPI ThreadProc(LPVOID lpParam);
  • DWORD WINAPI: 返回类型和调用约定。
  • LPVOID lpParam: 这是你在 CreateThread 中通过 lpParameter 传递进来的参数。

线程函数必须返回一个 DWORD 值,这个返回值可以通过 GetExitCodeThread 获取,通常用它来表示线程的执行状态或结果。

3 结束线程

  • 线程正常退出:线程函数执行到 return 语句。
  • 主动终止线程(不推荐):使用 TerminateThread,这会导致线程立即停止,资源无法正确释放,可能造成内存泄漏和数据不一致,应尽量避免使用。
  • 等待线程结束:使用 WaitForSingleObjectWaitForMultipleObjects 来等待一个或多个线程执行完毕,这是最推荐的方式。

4 获取当前线程信息

  • GetCurrentThread(): 获取当前线程的伪句柄,这个句柄不能被子进程继承,也不能用于需要句柄句柄的操作(如等待)。
  • GetCurrentThreadId(): 获取当前线程的 ID。

线程同步

多线程最大的挑战在于共享资源的访问,当多个线程同时读写同一个变量、同一个文件或同一个内存区域时,就会发生竞态条件,导致数据不一致或程序崩溃。

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

Windows 提供了多种同步对象来解决这个问题。

1 临界区

临界区是最快的同步机制,但它只能在单个进程内使用。

  • 工作方式:确保在任何时刻,只有一个线程可以进入被保护的代码区域。
  • 缺点:不能用于跨进程同步。

主要函数:

  • InitializeCriticalSection(&CriticalSection): 初始化临界区。
  • EnterCriticalSection(&CriticalSection) / TryEnterCriticalSection(&CriticalSection): 进入临界区,前者会阻塞,后者会立即返回。
  • LeaveCriticalSection(&CriticalSection): 离开临界区。
  • DeleteCriticalSection(&CriticalSection): 删除临界区,释放资源。

2 互斥量

互斥量与临界区类似,但它是内核对象,可以用于跨进程同步,因此速度比临界区慢。

  • 工作方式:与临界区类似,但拥有所有权概念,只有获得互斥量的线程才能释放它。
  • 优点:可用于进程间同步。

主要函数:

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

3 事件

事件是一种通知机制,线程可以等待某个事件的发生。

  • 工作方式:事件有两种状态:有信号无信号,线程可以等待事件变为有信号状态。
  • 类型
    • 自动重置事件:一个等待线程被释放后,事件会自动变为无信号状态。
    • 手动重置事件:必须手动调用 ResetEvent 才能将其变为无信号状态。

主要函数:

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

完整示例代码

下面是一个结合了线程创建、参数传递、同步(临界区)和等待的完整示例。

#include <windows.h>
#include <stdio.h>
// 定义一个全局变量,作为共享资源
long g_totalCount = 0;
// 定义一个临界区对象
CRITICAL_SECTION g_cs;
// 线程函数
DWORD WINAPI ThreadProc(LPVOID lpParam) {
    // 从参数中获取线程ID
    int threadId = (int)lpParam;
    int count = 0;
    // 每个线程循环10万次
    while (count < 100000) {
        // 进入临界区,保护对 g_totalCount 的访问
        EnterCriticalSection(&g_cs);
        g_totalCount++; // 原子操作,但在这里为了演示同步,我们仍然保护
        // 离开临界区
        LeaveCriticalSection(&g_cs);
        count++;
    }
    printf("Thread %d finished. It counted %d times.\n", threadId, count);
    return 0; // 线程正常退出
}
int main() {
    HANDLE hThreads[4]; // 用于保存线程句柄的数组
    DWORD dwThreadId[4]; // 用于保存线程ID的数组
    // 初始化临界区
    InitializeCriticalSection(&g_cs);
    printf("Main thread is creating 4 worker threads...\n");
    // 创建4个线程
    for (int i = 0; i < 4; i++) {
        // 将线程ID作为参数传递给线程函数
        hThreads[i] = CreateThread(
            NULL,                   // 默认安全属性
            0,                      // 默认堆栈大小
            ThreadProc,             // 线程函数
            (LPVOID)(i + 1),        // 传递给线程函数的参数 (线程ID)
            0,                      // 立即运行
            &dwThreadId[i]          // 用于接收线程ID
        );
        if (hThreads[i] == NULL) {
            printf("CreateThread failed (%d)\n", GetLastError());
            // 清理已创建的线程
            for (int j = 0; j < i; j++) {
                CloseHandle(hThreads[j]);
            }
            DeleteCriticalSection(&g_cs);
            return 1;
        }
    }
    printf("Main thread is waiting for all worker threads to finish...\n");
    // 等待所有线程执行完毕
    WaitForMultipleObjects(4, hThreads, TRUE, INFINITE);
    printf("All threads have finished. Final g_totalCount: %ld\n", g_totalCount);
    // 清理资源
    for (int i = 0; i < 4; i++) {
        CloseHandle(hThreads[i]); // 关闭线程句柄
    }
    // 删除临界区
    DeleteCriticalSection(&g_cs);
    return 0;
}

代码解释:

  1. 全局变量 g_totalCount:这是一个共享资源,所有线程都会尝试修改它。
  2. 全局变量 g_cs:这是一个临界区对象,用于保护 g_totalCount
  3. ThreadProc 函数:这是每个线程的执行体,它接收一个线程ID作为参数,在一个循环中递增 g_totalCount,在递增操作前后,使用 EnterCriticalSectionLeaveCriticalSection 来确保同一时间只有一个线程能修改 g_totalCount
  4. main 函数
    • InitializeCriticalSection: 初始化临界区。
    • 循环调用 CreateThread 创建4个线程,并将 i+1 作为线程ID传递。
    • WaitForMultipleObjects: 这是关键,它会让主线程阻塞,直到所有4个工作线程都执行完毕。
    • 关闭所有线程句柄并删除临界区,释放资源。

运行结果分析:

如果没有临界区,由于 g_totalCount++ 不是原子操作(它包含“读取-修改-写入”三步),g_totalCount 的值很可能不等于 4 * 100000 = 400000。加上临界区后,结果一定是 400000,因为它确保了操作的原子性。


最佳实践和注意事项

  1. 避免全局变量:虽然上面的例子用了全局变量,但在实际项目中,应尽量通过线程参数传递数据,或者使用线程本地存储来减少共享,降低耦合。
  2. 注意同步粒度:同步代码块(临界区/互斥量)的范围应尽可能小,只保护必要的共享资源,范围过大会降低并发性能。
  3. 防止死锁:死锁发生在两个或多个线程互相等待对方释放资源时,线程A持有锁1并等待锁2,而线程B持有锁2并等待锁1。避免死锁的原则是:
    • 按固定的顺序获取多个锁。
    • 尽量使用同一类型的同步原语。
    • 设置超时等待(WaitForSingleObjectdwMilliseconds 参数不要总是用 INFINITE)。
  4. 资源释放CreateThread 创建的句柄必须用 CloseHandle 关闭。CreateMutex 等创建的内核对象也必须关闭。malloc 分配的在线程中使用的内存,其释放责任必须明确,避免内存泄漏或悬挂指针。
  5. 使用 try-finallyRAII 模式:确保在异常或复杂逻辑下,LeaveCriticalSectionReleaseMutex 总是被调用,C语言中没有RAII,但你可以通过 goto 或在函数末尾添加 finally 代码块来模拟。
  6. 考虑使用 C11 线程库:如果你不依赖于 Windows 特有的 API,并且希望代码具有更好的可移植性,可以考虑使用 C11 标准引入的 <threads.h>,它提供了 thrd_create, mtx_lock, cnd_wait 等跨平台的线程和同步原语。

希望这份详细的指南能帮助你掌握 Windows C 语言多线程编程!

-- 展开阅读全文 --
头像
51单片机C语言开发技术如何入门精通?
« 上一篇 2025-12-22
织梦免费HTML模板下载地址哪里找?
下一篇 » 2025-12-22

相关文章

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

目录[+]