windows下c语言多线程编程

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

Windows 提供了一套强大的多线程 API,主要包含在 <windows.h> 头文件中,这套 API 非常成熟和稳定,是进行 Windows 平台多线程开发的基础。

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

核心概念与准备

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

  • 进程:一个应用程序的实例,拥有独立的内存空间和系统资源。
  • 线程:进程内的一个执行单元,是 CPU 调度的基本单位,一个进程可以包含多个线程,它们共享进程的资源(如内存、文件句柄等)。
  • 句柄:Windows 中用来标识资源的唯一整数,线程、进程、文件、窗口等都是资源,都有对应的句柄。
  • 同步:当多个线程访问共享资源时,需要采取某种机制来协调它们的执行顺序,以避免数据竞争和不一致的结果,常用的同步机制有:互斥量、临界区、事件、信号量等。

准备工作:

  1. 包含头文件:在你的 C 源文件中,必须包含 <windows.h>
    #include <windows.h>
    #include <stdio.h> // 用于 printf
  2. 链接库:Windows 的多线程 API 是操作系统自带的,在 Visual Studio 中创建项目时默认会链接所需的库(如 kernel32.lib),如果你使用其他编译器(如 MinGW),可能需要手动指定链接库,但通常也不需要。

创建和终止线程

1 创建线程:CreateThread

这是创建线程最核心的函数。

HANDLE CreateThread(
  LPSECURITY_ATTRIBUTES   lpThreadAttributes, // 线程安全属性,通常为 NULL
  SIZE_T                  dwStackSize,        // 线程栈大小,0 表示使用默认大小
  LPTHREAD_START_ROUTINE  lpStartAddress,     // 线程的起始函数地址
  LPVOID                  lpParameter,         // 传递给线程函数的参数
  DWORD                   dwCreationFlags,    // 创建标志,0 表示立即运行,CREATE_SUSPENDED 表示挂起
  LPDWORD                 lpThreadId           // 用于接收线程 ID 的指针,可以为 NULL
);
  • 返回值:如果成功,返回一个新线程的句柄;如果失败,返回 NULL,你需要通过 GetLastError() 获取错误码。
  • 线程函数lpStartAddress 指向的函数必须是以下原型:
    DWORD WINAPI ThreadFunction(LPVOID lpParam);
    • WINAPI 是一个调用约定宏,确保参数传递方式正确。
    • LPVOID lpParam 是一个指向任意类型数据的指针,你可以用它来向线程传递参数。
    • 函数的返回值 (DWORD) 会被 GetExitCodeThread 用来获取线程的退出码。

2 线程函数示例

// 线程函数
DWORD WINAPI ThreadFunc(LPVOID lpParam) {
    // 将 lpParam 转换为我们需要的类型
    int threadNum = *(int*)lpParam;
    for (int i = 0; i < 5; i++) {
        printf("Hello from thread %d! Count: %d\n", threadNum, i);
        Sleep(500); // 休眠 500 毫秒
    }
    // 线程结束,返回一个退出码
    return threadNum * 100;
}

3 等待线程结束:WaitForSingleObject

主线程需要一种方式来等待子线程完成,否则主线程可能执行完毕,导致整个进程退出,子线程被强制终止。

windows下c语言多线程编程
(图片来源网络,侵删)
DWORD WaitForSingleObject(
  HANDLE hHandle,    // 要等待的对象句柄(这里是线程句柄)
  DWORD  dwMilliseconds // 等待时间,INFINITE 表示无限等待
);
  • 返回值:
    • WAIT_OBJECT_0:对象 signaled 状态(对于线程,意味着已经结束)。
    • WAIT_TIMEOUT:等待超时。
    • WAIT_FAILED:等待失败。

4 获取线程退出码:GetExitCodeThread

BOOL GetExitCodeThread(
  HANDLE  hThread,     // 线程句柄
  LPDWORD lpExitCode   // 用于接收退出码的 DWORD 指针
);
  • 如果线程仍在运行,lpExitCode 会被设置为 STILL_ACTIVE

5 关闭线程句柄:CloseHandle

当你不再需要使用线程句柄时(在等待线程结束后),应该关闭它,以释放系统资源。

BOOL CloseHandle(HANDLE hObject);

完整示例:创建和等待线程

这个例子演示了如何创建两个线程,并等待它们完成。

#include <windows.h>
#include <stdio.h>
// 线程函数
DWORD WINAPI ThreadFunc(LPVOID lpParam) {
    int threadNum = *(int*)lpParam;
    for (int i = 0; i < 5; i++) {
        printf("Thread %d is running... Count: %d\n", threadNum, i);
        Sleep(500); // 休眠 500 毫秒
    }
    printf("Thread %d is finished.\n", threadNum);
    return threadNum * 100; // 返回一个退出码
}
int main() {
    HANDLE hThread1, hThread2;
    DWORD threadId1, threadId2;
    int param1 = 1, param2 = 2;
    DWORD exitCode1, exitCode2;
    // 创建第一个线程
    hThread1 = CreateThread(
        NULL,                   // 默认安全属性
        0,                      // 默认堆栈大小
        ThreadFunc,             // 线程函数
        &param1,                // 传递给线程函数的参数
        0,                      // 立即运行
        &threadId1              // 接收线程ID
    );
    if (hThread1 == NULL) {
        printf("CreateThread failed for thread 1. Error: %d\n", GetLastError());
        return 1;
    }
    // 创建第二个线程
    hThread2 = CreateThread(
        NULL,                   // 默认安全属性
        0,                      // 默认堆栈大小
        ThreadFunc,             // 线程函数
        &param2,                // 传递给线程函数的参数
        0,                      // 立即运行
        &threadId2              // 接收线程ID
    );
    if (hThread2 == NULL) {
        printf("CreateThread failed for thread 2. Error: %d\n", GetLastError());
        CloseHandle(hThread1); // 清理已创建的句柄
        return 1;
    }
    printf("Main thread: Created threads %d and %d.\n", threadId1, threadId2);
    // 等待两个线程都执行完毕
    WaitForSingleObject(hThread1, INFINITE);
    WaitForSingleObject(hThread2, INFINITE);
    printf("Main thread: Both threads have finished.\n");
    // 获取并打印线程的退出码
    GetExitCodeThread(hThread1, &exitCode1);
    GetExitCodeThread(hThread2, &exitCode2);
    printf("Thread 1 exited with code: %d\n", exitCode1);
    printf("Thread 2 exited with code: %d\n", exitCode2);
    // 关闭线程句柄
    CloseHandle(hThread1);
    CloseHandle(hThread2);
    return 0;
}

编译与运行 (Visual Studio):

  1. 创建一个新的 "Windows Desktop Application" 项目。
  2. 将上述代码粘贴到 main.c 文件中。
  3. F5 运行。

编译与运行 (MinGW/GCC):

windows下c语言多线程编程
(图片来源网络,侵删)
gcc -o thread_example thread_example.c -lkernel32
./thread_example

线程同步

当多个线程需要访问同一个共享资源(如一个全局变量)时,就会发生数据竞争,导致不可预知的结果,Windows 提供了多种同步对象来解决此问题。

1 临界区

临界区是最快的同步机制,但它只能用于同一进程内的线程同步,它不能跨进程使用。

  • 特点:速度快,占用资源少。
  • 使用
    1. 声明一个 CRITICAL_SECTION 变量。
    2. 初始化 InitializeCriticalSection
    3. 进入临界区 EnterCriticalSection(会阻塞)。
    4. 离开临界区 LeaveCriticalSection
    5. 不用时删除 DeleteCriticalSection

示例:

#include <windows.h>
#include <stdio.h>
long g_sharedCounter = 0;
CRITICAL_SECTION g_cs; // 声明临界区
DWORD WINAPI CounterThread(LPVOID lpParam) {
    for (int i = 0; i < 100000; i++) {
        EnterCriticalSection(&g_cs); // 进入临界区
        g_sharedCounter++; // 安全地修改共享数据
        LeaveCriticalSection(&g_cs); // 离开临界区
    }
    return 0;
}
int main() {
    HANDLE hThread[4];
    InitializeCriticalSection(&g_cs); // 初始化临界区
    // 创建4个线程
    for (int i = 0; i < 4; i++) {
        hThread[i] = CreateThread(NULL, 0, CounterThread, NULL, 0, NULL);
    }
    // 等待所有线程完成
    WaitForMultipleObjects(4, hThread, TRUE, INFINITE);
    // 关闭句柄
    for (int i = 0; i < 4; i++) {
        CloseHandle(hThread[i]);
    }
    printf("Final counter value: %ld\n", g_sharedCounter); // 应该是 400000
    DeleteCriticalSection(&g_cs); // 删除临界区
    return 0;
}

2 互斥量

互斥量与临界区功能类似,但它是一个内核对象,可以用于跨进程同步,这意味着它比临界区慢,但功能更强大。

  • 特点:可以跨进程,速度比临界区慢。
  • 使用
    1. 声明一个 HANDLE 类型的互斥量变量。
    2. 创建 CreateMutex
    3. 等待互斥量 WaitForSingleObject
    4. 释放互斥量 ReleaseMutex
    5. 关闭句柄 CloseHandle

示例: (代码结构与临界区示例类似,只需将 CRITICAL_SECTION 相关的调用替换为 HANDLE 互斥量的相关调用)

HANDLE g_hMutex;
// 在线程函数中
WaitForSingleObject(g_hMutex, INFINITE); // 等待
g_sharedCounter++;
ReleaseMutex(g_hMutex); // 释放
// 在 main 函数中
g_hMutex = CreateMutex(NULL, FALSE, NULL); // 创建
// ... 等待线程 ...
CloseHandle(g_hMutex); // 关闭

3 信号量

信号量用于控制同时访问某个资源的线程数量,它维护一个计数器。

  • 特点:允许多个线程同时访问,但数量有限。
  • 使用
    1. 创建 CreateSemaphore,指定初始计数和最大计数。
    2. 等待信号量 WaitForSingleObject(计数减1)。
    3. 释放信号量 ReleaseSemaphore(计数加1)。

4 事件

事件用于线程间的通知,一个线程可以等待一个事件被“触发”(signaled)。

  • 特点:用于通知,而不是锁定。
  • 使用
    1. 创建 CreateEvent
    2. 等待事件 WaitForSingleObject
    3. 触发事件 SetEvent(手动重置)或 PulseEvent(脉冲)。

线程终止

  • 自然退出:线程函数执行完毕并返回,这是最推荐的方式。
  • ExitThread:线程主动调用此函数退出。这会导致栈上的局部变量不被清理,通常不推荐使用。
  • TerminateThread:强制终止一个线程。这是非常危险的操作! 它不会释放线程拥有的资源(如锁、内存句柄等),可能导致整个进程处于不稳定状态,应仅在极端紧急情况下使用。

现代 C++ 的替代方案 (C++11)

如果你使用的是支持 C++11 或更高版本的编译器(如 Visual Studio 2012+ 或 GCC 4.8+),强烈推荐使用 C++11 的 <thread>,它提供了更安全、更面向对象、跨平台的接口,避免了 Windows API 中容易出错的句柄管理问题。

C++11 示例:

#include <iostream>
#include <thread>
#include <vector>
void thread_func(int id) {
    for (int i = 0; i < 5; ++i) {
        std::cout << "Hello from thread " << id << "! Count: " << i << std::endl;
        std::this_thread::sleep_for(std::chrono::milliseconds(500));
    }
}
int main() {
    std::vector<std::thread> threads;
    // 创建并启动多个线程
    for (int i = 1; i <= 4; ++i) {
        threads.emplace_back(thread_func, i);
    }
    std::cout << "Main thread: Created 4 threads." << std::endl;
    // 等待所有线程完成
    for (auto& th : threads) {
        if (th.joinable()) {
            th.join();
        }
    }
    std::cout << "Main thread: All threads have finished." << std::endl;
    return 0;
}

C++11 的优势:

  • 自动管理std::thread 对象在析构时会自动 join()(如果设置了 joinable)。
  • 类型安全:传递参数时类型安全,避免了 LPVOID 的强制转换。
  • 跨平台:同样的代码可以在 Linux 和 macOS 上编译运行。
  • 丰富的库:提供了 std::mutex, std::lock_guard, std::future, std::async 等高级并发工具。

特性 Windows API (<windows.h>) C++11 (<thread>)
易用性 较低,需要手动管理句柄和错误检查 高,面向对象,RAII(资源获取即初始化)
安全性 较低,容易忘记 CloseHandleWaitFor... 高,对象生命周期管理清晰
跨平台 否,仅限 Windows 是,代码可移植到 Linux, macOS 等
性能 原生 API,性能极佳 通常有极小的额外开销,但现代实现已非常高效
推荐场景 维护旧的 Windows C 代码。
需要使用 Windows 特有的高级功能(如 I/O 完成端口)。
新项目,尤其是 C++ 项目。
需要代码可移植性。
希望使用更现代、更安全的并发模型。

对于新的 C++ 项目,请优先选择 C++11 的 <thread> 库,对于纯 C 语言项目或需要深度集成 Windows 平台的场景,Windows API 依然是强大而可靠的选择。

-- 展开阅读全文 --
头像
UNIX平台下C语言高级编程指南
« 上一篇 2025-12-22
C语言如何实现base64编码?
下一篇 » 2025-12-22

相关文章

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

目录[+]