C++ 多线程同步之互斥锁(mutex)实战

在多线程编程中,多个线程可能同时访问共享资源(如全局变量或数据结构),导致数据竞争和不确定行为。互斥锁(mutex)是一种同步机制,用于确保在任何时刻只有一个线程能访问关键资源,从而避免竞争条件。C++11 标准库提供了 std::mutex 类来实现互斥锁。下面我将通过一个实战示例逐步解释如何使用互斥锁。

为什么需要互斥锁?
  • 当多个线程并发修改共享变量时,可能发生数据竞争。例如,两个线程同时增加一个计数器变量,由于操作非原子性,最终结果可能错误。
  • 互斥锁通过锁定和解锁操作来保护临界区(critical section),确保线程安全访问。
C++ 中的互斥锁实现

C++ 标准库中的 std::mutex 提供基本锁定功能:

  • lock(): 锁定互斥锁,如果已被锁定则阻塞当前线程。
  • unlock(): 解锁互斥锁。
  • 推荐使用 RAII(Resource Acquisition Is Initialization)风格的包装器,如 std::lock_guardstd::unique_lock,自动管理锁的生命周期,避免手动解锁错误。
实战示例:共享计数器保护

以下是一个完整的 C++ 示例,展示两个线程同时增加一个共享计数器,使用互斥锁确保正确同步:

#include <iostream>
#include <thread>
#include <mutex>
#include <vector>

// 共享资源
int counter = 0;
std::mutex mtx;  // 全局互斥锁

// 线程函数:增加计数器
void increment_counter(int id) {
    for (int i = 0; i < 10000; ++i) {
        std::lock_guard<std::mutex> lock(mtx);  // 自动锁定,作用域结束自动解锁
        ++counter;  // 安全访问共享资源
        // 可选:打印线程信息(实际应用中可能影响性能)
        // std::cout << "Thread " << id << " incremented to " << counter << std::endl;
    }
}

int main() {
    const int num_threads = 2;
    std::vector<std::thread> threads;

    // 创建并启动线程
    for (int i = 0; i < num_threads; ++i) {
        threads.emplace_back(increment_counter, i);
    }

    // 等待所有线程完成
    for (auto& t : threads) {
        t.join();
    }

    // 输出最终结果
    std::cout << "Final counter value: " << counter << std::endl;  // 应为 20000
    return 0;
}
代码关键点解释
  1. 共享资源定义:

    • counter 是一个全局整数,作为共享变量。
    • mtx 是一个全局 std::mutex 对象,用于保护对 counter 的访问。
  2. 线程函数:

    • increment_counter 函数在每个线程中运行,循环增加计数器。
    • 使用 std::lock_guard<std::mutex> lock(mtx) 自动锁定互斥锁:当 lock_guard 对象创建时,它调用 mtx.lock();当函数作用域结束时(例如循环结束),它自动调用 mtx.unlock()。这确保了异常安全,避免死锁。
  3. 创建和同步线程:

    • main 函数中,创建多个线程并存储到 std::vector
    • 使用 join() 等待所有线程完成,确保正确输出最终结果。
    • 预期输出:Final counter value: 20000(因为每个线程增加 10000 次,总 20000)。如果没有互斥锁,结果可能小于 20000 且不确定。
最佳实践和注意事项
  1. 使用 RAII 包装器:

    • 优先选择 std::lock_guard(简单场景)或 std::unique_lock(支持延迟锁定和条件变量)。手动调用 lock()unlock() 易出错,可能导致死锁或资源泄漏。
    • 示例:std::unique_lock<std::mutex> lock(mtx, std::defer_lock); 允许更灵活的控制。
  2. 避免死锁:

    • 死锁发生在多个锁以不一致顺序获取时。确保所有线程以相同顺序锁定互斥锁。
    • 使用 std::lock() 函数同时锁定多个互斥锁,避免循环等待。
  3. 性能考虑:

    • 互斥锁引入开销,临界区应尽量短小。避免在锁内执行耗时操作(如 I/O)。
    • 如果共享资源访问频繁,考虑原子操作(std::atomic)或无锁数据结构。
  4. 异常安全:

    • RAII 风格(如 lock_guard)保证即使抛出异常,锁也会释放。
    • 避免在锁内抛出异常;如果必须,使用 try-catch 块确保解锁。
  5. 测试和调试:

    • 编译时使用 C++11 或更高标准:g++ -std=c++11 -pthread your_code.cpp -o output
    • 运行多次测试验证结果一致性。使用工具如 Valgrind 检测数据竞争。

通过这个实战示例,你可以快速掌握互斥锁在多线程同步中的应用。记住,同步机制的选择取决于具体场景,互斥锁是基础但强大的工具。在实际项目中,结合条件变量(std::condition_variable)可以实现更复杂的线程协调。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐