C++并发学习笔记(4):锁

锁(`mutex`)是 C++ 中用于保护共享资源的线程同步工具,防止多个线程同时访问同一资源而导致数据竞争(data race)或不一致的问题。`mutex` 是 "mutual exclusion" 的缩写,意为互斥锁。它的核心功能是确保同一时间只有一个线程可以访问被保护的资源。

基本概念

  • 锁的加锁:当一个线程需要访问共享资源时,它需要先锁定(lock)对应的 mutex。加锁成功后,其他线程将无法再获得这把锁,必须等待锁被释放。
  • 锁的解锁:访问完共享资源后,线程需要解锁(unlockmutex,释放锁以允许其他线程访问资源。

常用类型

  1. std::mutex
    最常用的互斥锁,支持显式的 lock()unlock() 方法。

  2. std::timed_mutex
    支持定时等待的互斥锁,可以使用 try_lock_fortry_lock_until 来尝试在一段时间内获取锁。

  3. std::recursive_mutex
    支持同一线程多次加锁,但需要确保每次加锁都要有对应的解锁。

  4. std::shared_mutex(C++17 引入)
    提供共享锁和独占锁的能力,适用于读多写少的场景。

  5. std::unique_lock 和 std::lock_guard
    这两个是管理 mutex 的 RAII 工具,能自动加锁和解锁,减少忘记解锁的风险。

在基础功能之上,C++ 中的其他锁类(如 std::recursive_mutex, std::timed_mutex, std::shared_mutex 等)主要在以下三个方面进行了扩展:

1. 锁的重入性(Reentrancy): 适合递归函数需要加锁的情况

  • 扩展说明:支持同一线程多次加锁,这是由 std::recursive_mutex 提供的功能。

    • 如果线程 A 已经持有锁,再次尝试获取同一个锁不会导致死锁,计数器会递增。
    • 每次加锁都需要匹配对应的解锁,计数器归零时锁才会被释放。
  • 示例代码

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

std::recursive_mutex r_mtx;

void recursive_function(int count) {
    if (count <= 0) return;

    r_mtx.lock();
    std::cout << "Lock acquired in recursive call " << count << std::endl;
    recursive_function(count - 1);
    r_mtx.unlock();
}

int main() {
    std::thread t1(recursive_function, 5);
    t1.join();
    return 0;
}

若同一线程需要反复访问受保护(protected)的资源,也同样适用

2. 定时锁(Timed Locking):字面意思,避免线程长时间阻塞

  • 扩展说明:提供超时机制,允许线程在一定时间内尝试获取锁,由 std::timed_mutexstd::shared_timed_mutex 提供。

    • try_lock_for:指定持续的超时时间。
    • try_lock_until:指定绝对时间点。
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>

std::timed_mutex t_mtx;

void try_timed_lock() {
    if (t_mtx.try_lock_for(std::chrono::milliseconds(100))) {
        std::cout << "Lock acquired by thread " << std::this_thread::get_id() << std::endl;
        std::this_thread::sleep_for(std::chrono::milliseconds(200));
        t_mtx.unlock();
    } else {
        std::cout << "Thread " << std::this_thread::get_id() << " could not acquire lock." << std::endl;
    }
}

int main() {
    std::thread t1(try_timed_lock);
    std::thread t2(try_timed_lock);

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

    return 0;
}

3. 共享锁(Shared Locking): 允许多个线程同时读取

  • 扩展说明:支持读写锁功能,由 std::shared_mutex 提供。

    • 独占锁(unique_lock):用于写操作,只有一个线程可以获得。
    • 共享锁(shared_lock):用于读操作,多个线程可以同时持有共享锁。
#include <iostream>
#include <thread>
#include <shared_mutex>
#include <mutex>
#include <vector>

std::shared_mutex s_mtx;    // 用于保护 shared_data 的共享锁
std::mutex cout_mtx;        // 用于保护 std::cout 的独占锁
int shared_data = 0;

void writer() {
    int input_value;
    std::cout << "Writer thread " << std::this_thread::get_id() << ", please enter a value: ";
    std::cin >> input_value;

    std::unique_lock<std::shared_mutex> lock(s_mtx);  // 独占锁
    shared_data = input_value;

    {
        std::lock_guard<std::mutex> cout_lock(cout_mtx);  // 保护 std::cout
        std::cout << "Writer thread " << std::this_thread::get_id() 
                  << " updated shared_data to " << shared_data << std::endl;
    }
}

void reader(int id) {
    std::shared_lock<std::shared_mutex> lock(s_mtx);  // 共享锁
    {
        std::lock_guard<std::mutex> cout_lock(cout_mtx);  // 保护 std::cout
        std::cout << "Reader " << id << " (Thread " << std::this_thread::get_id() 
                  << ") reads shared_data: " << shared_data << std::endl;
    }
}

int main() {
    std::thread t1(writer);

    // 启动多个 reader 线程
    std::vector<std::thread> readers;
    for (int i = 1; i <= 3; ++i) {
        readers.emplace_back(reader, i);
    }

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

    return 0;
}

有的读者可能会注意到一些问题:reader和writer共用一个smtx

为什么需要共用一个锁?

  1. 共享资源的一致性

    • writer 修改共享数据时,reader 必须等到 writer 完成修改后才能读取,确保读取到的数据是最新的。
    • reader 并发访问时,多个线程同时读取不会改变数据,因此可以安全地共享锁,而无需相互等待。
  2. 避免竞争条件

    • 如果没有锁保护,writerreader 可能同时访问 shared_data,导致以下问题:
      • readerwriter 写入未完成时读取了不完整的数据(数据竞争)。
      • writer 在多个 reader 同时访问时强行写入,导致未定义行为。
  3. 锁的类型支持读多写少的场景

    • 使用 std::shared_mutex,允许:
      • 多个 reader 同时持有 共享锁
      • 一个 writer 独占持有 独占锁,阻止其他 readerwriter
    • 这种机制适合读多写少的场景,比如配置文件读取、缓存系统等。

但是你会得到如下结果:reader在writer前先行执行,导致读入值为0
![[Pasted image 20250116173101.png]]

问题分析

  1. 线程调度的不确定性

    • readerwriter 是并发执行的,具体哪个线程先运行是由系统线程调度决定的。你现在的输出表明:
      • reader 线程在 writer 完成写入之前就读取了 shared_data,因此读取到的是初始值 0
  2. std::shared_mutex 的保护范围

    • 目前 std::shared_mutex 确保了单个线程对 shared_data 的操作是安全的,但无法保证 reader 等待 writer 完成更新后再开始读取。
    • 换句话说,锁本身无法提供线程之间的执行顺序。

解决方法:std::condition_variable

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

std::shared_mutex s_mtx;               // 用于保护 shared_data 的共享锁
std::mutex cv_mtx;                     // 用于保护条件变量
std::condition_variable cv;            // 条件变量
bool data_ready = false;               // 标志共享数据是否更新
int shared_data = 0;

void writer() {
    int input_value;
    std::cout << "Writer thread " << std::this_thread::get_id() << ", please enter a value: ";
    std::cin >> input_value;

    {
        std::unique_lock<std::shared_mutex> lock(s_mtx);  // 独占锁
        shared_data = input_value;
    }

    {
        std::lock_guard<std::mutex> lock(cv_mtx);         // 保护条件变量
        data_ready = true;                                // 标志数据已更新
    }
    cv.notify_all();                                      // 通知所有等待线程

    std::cout << "Writer thread " << std::this_thread::get_id() 
              << " updated shared_data to " << shared_data << std::endl;
}

void reader(int id) {
    {
        std::unique_lock<std::mutex> lock(cv_mtx);
        cv.wait(lock, [] { return data_ready; });         // 等待数据更新
    }

    std::shared_lock<std::shared_mutex> lock(s_mtx);      // 共享锁读取数据
    std::cout << "Reader " << id << " (Thread " << std::this_thread::get_id() 
              << ") reads shared_data: " << shared_data << std::endl;
}

int main() {
    std::thread t1(writer);

    // 启动多个 reader 线程
    std::vector<std::thread> readers;
    for (int i = 1; i <= 3; ++i) {
        readers.emplace_back(reader, i);
    }

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

    return 0;
}

std::unique_lockstd::condition_variable 的配合使用

std::unique_lockstd::lock_guard 更适合用于与 std::condition_variable 配合使用,因为它能够在等待时释放锁,并在唤醒后自动重新获取锁。

生产者-消费者模式

工作原理

  1. 等待线程(消费者)

    • 调用 wait 方法挂起线程,直到某个条件为真。
    • wait 方法需要配合锁(std::unique_lock)使用,以便在等待时保护共享资源。
  2. 通知线程(生产者)

    • 调用 notify_onenotify_all 方法,通知一个或所有等待线程重新检查条件。
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>

std::queue<int> data_queue;          // 数据队列
std::mutex mtx;                      // 用于保护队列的互斥锁
std::condition_variable cv;          // 条件变量
bool done = false;                   // 标志数据是否生成完成

// 消费者线程:处理数据
void consumer() {
    while (true) {
        std::unique_lock<std::mutex> lock(mtx); // 获取锁
        cv.wait(lock, [] { return !data_queue.empty() || done; }); // 等待条件

        while (!data_queue.empty()) {
            int data = data_queue.front();
            data_queue.pop();
            std::cout << "Consumed: " << data << std::endl;
        }

        if (done) break; // 数据生成完成,退出循环
    }
}

// 生产者线程:生成数据
void producer() {
    for (int i = 1; i <= 5; ++i) {
        {
            std::lock_guard<std::mutex> lock(mtx); // 加锁
            data_queue.push(i);                   // 生成数据
            std::cout << "Produced: " << i << std::endl;
        }
        cv.notify_one(); // 通知消费者
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
    {
        std::lock_guard<std::mutex> lock(mtx);
        done = true; // 标记数据生成完成
    }
    cv.notify_all(); // 通知所有等待线程
}

int main() {
    std::thread t1(consumer);
    std::thread t2(producer);

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

    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值