C 语言

关注公众号 jb51net

关闭
首页 > 软件编程 > C 语言 > C++线程锁

C++线程锁的使用

作者:Ralph_Y

C++提供了多种锁机制,本文主要介绍了C++线程锁的使用,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

一、基础概念:锁的核心分类

在讲解具体工具前,先明确C++锁的两个核心维度:

  1. 基础锁类型(提供原始的加锁/解锁能力):std::mutex、std::recursive_mutex、std::timed_mutex、std::recursive_timed_mutex;
  2. RAII锁封装(管理基础锁的生命周期,避免手动解锁漏解/死锁):std::lock_guard、std::unique_lock、std::scoped_lock(C++17)。

二、基础锁类型(原始锁)

这类锁是“底层同步工具”,提供lock()、unlock()、try_lock()等核心方法,但不建议直接使用(手动解锁易出错),需配合RAII封装使用。

锁类型核心特性适用场景注意事项
std::mutex最基础的互斥锁,非递归、非超时绝大多数单资源互斥场景同一线程重复lock()会触发未定义行为(崩溃)
std::recursive_mutex递归互斥锁,同一线程可多次lock()(需对应次数unlock())函数嵌套调用需要加锁的场景性能略低于std::mutex,避免滥用
std::timed_mutex带超时的互斥锁,支持try_lock_for()(超时时间)、try_lock_until()(截止时间)需要“非阻塞等待锁”的场景超时返回false,避免永久阻塞
std::recursive_timed_mutex递归+超时的互斥锁,结合前两者特性递归调用+超时等待的场景尽量少用,递归锁易隐藏逻辑问题

基础锁使用示例(不推荐直接用,仅演示)

#include <mutex>
#include <iostream>
std::mutex m;

void bad_use() {
    m.lock(); // 手动加锁
    std::cout << "临界区操作" << std::endl;
    // 若此处抛异常,unlock()不会执行 → 死锁!
    m.unlock(); // 手动解锁
}

核心问题:手动管理lock()/unlock()易因异常、逻辑分支遗漏解锁,导致死锁——这也是RAII封装的核心价值。

三、RAII锁封装(推荐使用)

RAII(资源获取即初始化)的核心思想:构造时获取资源,析构时释放资源。锁封装会在构造函数中调用lock()(或接管已锁定的锁),析构函数中调用unlock(),无论函数正常退出还是异常退出,锁都会被释放。

1. std::lock_guard(极简RAII锁,C++11)

核心特性

使用示例(最常用场景)

#include <mutex>
#include <list>
std::list<int> data;
std::mutex m;

void safe_add(int val) {
    // 构造时加锁,函数结束析构时解锁
    std::lock_guard<std::mutex> guard(m);
    data.push_back(val); // 临界区操作,线程安全
}

进阶用法:接管已锁定的锁(std::adopt_lock)

当锁已被std::lock()手动锁定时,用std::adopt_lock标记告诉lock_guard“锁已锁定,只需接管解锁责任”:

void swap_data(std::list<int>& a, std::list<int>& b, std::mutex& m1, std::mutex& m2) {
    if (&a == &b) return;
    std::lock(m1, m2); // 同时锁定两个锁,避免死锁
    // adopt_lock:不重复加锁,仅接管解锁
    std::lock_guard<std::mutex> g1(m1, std::adopt_lock);
    std::lock_guard<std::mutex> g2(m2, std::adopt_lock);
    a.swap(b); // 临界区操作
}

适用场景

2. std::unique_lock(灵活RAII锁,C++11)

核心特性

核心用法示例

(1)延迟加锁(std::defer_lock)

#include <mutex>
std::timed_mutex tm;

void flexible_lock() {
    // defer_lock:构造时不加锁,仅关联锁对象
    std::unique_lock<std::timed_mutex> ul(tm, std::defer_lock);
    
    // 手动加锁(也可尝试超时加锁)
    if (ul.try_lock_for(std::chrono::seconds(1))) {
        std::cout << "获取锁成功,执行临界区操作" << std::endl;
        ul.unlock(); // 手动解锁(可提前释放锁,提高并发)
        // ... 其他非临界区操作
        ul.lock(); // 重新加锁
    } else {
        std::cout << "1秒内未获取锁,执行降级逻辑" << std::endl;
    }
    // 析构时:如果锁仍被持有,自动解锁
}

(2)配合条件变量(唯一适用场景)

std::condition_variablewait()方法必须接收std::unique_lock(因为wait()会临时解锁,被唤醒后重新加锁,lock_guard无此能力):

#include <mutex>
#include <condition_variable>
#include <queue>

std::queue<int> q;
std::mutex m;
std::condition_variable cv;

void producer(int val) {
    std::lock_guard<std::mutex> lg(m);
    q.push(val);
    cv.notify_one(); // 通知消费者
}

void consumer() {
    std::unique_lock<std::mutex> ul(m);
    // wait()会解锁,等待通知;被唤醒后重新加锁,再判断条件
    cv.wait(ul, [](){ return !q.empty(); });
    int val = q.front();
    q.pop();
    ul.unlock(); // 提前解锁,提高并发
    std::cout << "消费:" << val << std::endl;
}

适用场景

3. std::scoped_lock(C++17,lock_guard的升级版)

核心特性

使用示例(替代lock_guard+std::lock)

#include <mutex>
#include <list>

std::list<int> a, b;
std::mutex m1, m2;

void safe_swap() {
    // 同时锁定m1和m2,自动避免死锁,析构时同时解锁
    std::scoped_lock guard(m1, m2);
    a.swap(b); // 临界区操作
}

对比旧写法(lock_guard+std::lock):

std::lock(m1, m2);
std::lock_guard<std::mutex> g1(m1, std::adopt_lock);
std::lock_guard<std::mutex> g2(m2, std::adopt_lock);

scoped_lock一行搞定,更简洁、不易出错。

适用场景

四、其他锁相关工具

1. std::call_once(单次调用锁)

使用示例

#include <mutex>
#include <iostream>

std::once_flag flag;
void init() {
    std::cout << "仅执行一次的初始化逻辑" << std::endl;
}

void thread_func() {
    std::call_once(flag, init); // 多线程调用,init仅执行一次
}

2. std::shared_mutex / std::shared_lock(C++17,读写锁)

使用示例

#include <shared_mutex>
#include <string>
#include <iostream>

std::string data = "初始数据";
std::shared_mutex sm;

// 读线程:共享锁
void read_data(int id) {
    std::shared_lock<std::shared_mutex> sl(sm);
    std::cout << "读线程" << id << ":" << data << std::endl;
}

// 写线程:独占锁
void write_data(const std::string& new_data) {
    std::unique_lock<std::shared_mutex> ul(sm);
    data = new_data;
    std::cout << "写线程:更新数据为" << new_data << std::endl;
}

优势:读多写少场景下,并发效率远高于普通互斥锁(普通锁无论读写都独占)。

3. std::lock(通用锁函数)

五、核心对比与选型建议

工具核心优势核心限制优先选型场景
std::lock_guard极简、高效不可手动解锁、不支持多锁C++11/14,单锁、简单作用域加锁
std::unique_lock灵活(延迟/超时/手动解锁)略高开销(可忽略)条件变量、超时等待、动态解锁
std::scoped_lock多锁、简洁、高效C++17+C++17+,单锁/多锁、所有简单场景
std::shared_mutex读写分离,读多写少并发高C++17+读多写少的共享资源访问
std::call_once单次调用,无需手动加锁判断仅单次执行初始化、单例创建

选型口诀

  1. C++17及以上:优先用scoped_lock(单锁/多锁),复杂逻辑用unique_lock;
  2. C++11/14:单锁用lock_guard,多锁用std::lock + lock_guard,复杂逻辑用unique_lock;
  3. 读多写少:用std::shared_mutex + shared_lock/unique_lock;
  4. 单次初始化:用std::call_once;
  5. 条件变量:必须用unique_lock。

六、总结

  1. 基础锁:std::mutex是核心,递归场景用recursive_mutex,超时场景用timed_mutex;
  2. RAII封装:
    • 简单场景:lock_guard(C++11/14)/scoped_lock(C++17+);
    • 复杂场景:unique_lock(灵活、支持条件变量/超时);
  3. 特殊场景:
    • 读多写少:shared_mutex + shared_lock;
    • 单次执行:call_once;
  4. 核心原则:永远用RAII封装管理锁,避免手动lock()/unlock(),防止死锁。

到此这篇关于C++线程锁的使用的文章就介绍到这了,更多相关C++线程锁内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文