C++中的锁(Locks)机制是什么?如何使用std::mutex进行同步?

讲座主题:C++中的锁机制与std::mutex同步入门

大家好,欢迎来到今天的讲座!今天我们将一起探讨C++中的锁(Locks)机制,以及如何使用std::mutex进行线程同步。如果你对多线程编程感到头疼,别担心,我会用轻松幽默的方式带你一步步掌握这个知识点。


为什么我们需要锁?

在单线程的世界里,程序按部就班地执行,一切井然有序。然而,当我们进入多线程领域时,事情变得复杂起来。多个线程可能会同时访问共享资源(比如一个变量或数据结构),这就可能导致竞态条件(Race Condition)——即线程之间的操作顺序不确定,导致结果不可预测。

举个例子,假设你和你的朋友同时去银行取钱,你们的账户余额是100元。如果两个线程分别试图从账户中取出50元,但没有正确同步,可能会出现以下情况:

  • 线程A读取余额为100元。
  • 线程B也读取余额为100元。
  • 线程A扣除50元后更新余额为50元。
  • 线程B扣除50元后更新余额为50元。

最终,账户余额变成了50元,但实际上应该为0元!这就是竞态条件的一个典型例子。

为了避免这种情况,我们需要一种机制来确保同一时间只有一个线程可以访问共享资源。这就是锁的作用!


C++中的锁机制

在C++中,标准库提供了std::mutex作为最基本的锁机制。mutex是“Mutual Exclusion”(互斥)的缩写,它的作用就是让多个线程之间互相排斥,确保某个代码段在同一时刻只能被一个线程执行。

如何使用std::mutex

让我们通过一个简单的例子来学习如何使用std::mutex

示例代码:保护共享资源

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

// 创建一个全局变量作为共享资源
int sharedResource = 0;

// 定义一个mutex对象
std::mutex mtx;

void increment() {
    // 加锁
    mtx.lock();
    for (int i = 0; i < 1000; ++i) {
        ++sharedResource;
    }
    // 解锁
    mtx.unlock();
}

int main() {
    std::vector<std::thread> threads;

    // 创建多个线程,每个线程都会修改sharedResource
    for (int i = 0; i < 10; ++i) {
        threads.emplace_back(increment);
    }

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

    std::cout << "Final value of sharedResource: " << sharedResource << std::endl;

    return 0;
}

在这个例子中,我们创建了一个std::mutex对象mtx,并通过lock()unlock()方法手动控制锁的获取和释放。这样可以确保每次只有一个线程能够进入increment()函数中的临界区(Critical Section)。


更优雅的锁管理:std::lock_guard

虽然lock()unlock()可以实现基本的同步,但手动管理锁容易出错,比如忘记解锁或在异常情况下未能正确释放锁。为了解决这个问题,C++提供了std::lock_guard,它是一种RAII(Resource Acquisition Is Initialization)风格的工具,可以在构造时自动加锁,在析构时自动解锁。

使用std::lock_guard改写上面的例子

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

int sharedResource = 0;
std::mutex mtx;

void increment() {
    // 使用lock_guard自动管理锁
    std::lock_guard<std::mutex> lock(mtx);
    for (int i = 0; i < 1000; ++i) {
        ++sharedResource;
    }
}

int main() {
    std::vector<std::thread> threads;

    for (int i = 0; i < 10; ++i) {
        threads.emplace_back(increment);
    }

    for (auto& t : threads) {
        t.join();
    }

    std::cout << "Final value of sharedResource: " << sharedResource << std::endl;

    return 0;
}

在这个版本中,std::lock_guard会在其作用域结束时自动释放锁,避免了手动管理锁的风险。


多个锁的死锁问题

当多个线程需要同时获取多个锁时,可能会出现死锁(Deadlock)的情况。例如,线程A先获取锁1再获取锁2,而线程B先获取锁2再获取锁1,这会导致两个线程互相等待对方释放锁,从而陷入僵局。

如何避免死锁?

  1. 始终以相同的顺序获取锁:确保所有线程都按照相同的顺序获取锁。
  2. 使用std::lockstd::scoped_lock:C++提供了更高级的工具来帮助管理多个锁。

示例:使用std::lock避免死锁

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

std::mutex mtx1, mtx2;

void threadFunction(int id) {
    if (id == 1) {
        // 线程1先获取mtx1再获取mtx2
        std::lock(mtx1, mtx2); // 同时锁定两个锁
        std::lock_guard<std::mutex> lock1(mtx1, std::adopt_lock);
        std::lock_guard<std::mutex> lock2(mtx2, std::adopt_lock);
        std::cout << "Thread 1 locked both mutexes." << std::endl;
    } else {
        // 线程2先获取mtx2再获取mtx1
        std::lock(mtx2, mtx1); // 同时锁定两个锁
        std::lock_guard<std::mutex> lock1(mtx2, std::adopt_lock);
        std::lock_guard<std::mutex> lock2(mtx1, std::adopt_lock);
        std::cout << "Thread 2 locked both mutexes." << std::endl;
    }
}

int main() {
    std::thread t1(threadFunction, 1);
    std::thread t2(threadFunction, 2);

    t1.join();
    t2.join();

    return 0;
}

在这个例子中,std::lock确保了两个锁被同时锁定,从而避免了死锁。


总结

今天我们学习了C++中的锁机制,重点介绍了std::mutexstd::lock_guard的基本用法。以下是关键点的总结:

概念 描述
std::mutex 基本的互斥锁,用于保护共享资源。
std::lock_guard RAII风格的锁管理工具,简化锁的使用并防止忘记解锁。
死锁 多个线程互相等待对方释放锁,导致程序无法继续运行。
std::lock 用于同时锁定多个锁,避免死锁。

希望今天的讲座对你有所帮助!如果你有任何问题,欢迎随时提问。下次见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注