多线程锁基础

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

文章目录


你想从零开始学习C++多线程中的锁,希望我用基础、简单的方式讲解C++里有哪些锁、它们的使用方法,以及不同场景该选择哪种锁。我会从最基础的概念讲起,用简单的代码示例帮你理解,避免复杂术语。

先理解核心:为什么需要锁?

多线程程序中,多个线程同时访问共享资源 (比如一个全局变量、一个结构体)时,会出现"数据竞争"问题。比如两个线程同时给 count++,最终结果可能不对。锁的作用就是保证同一时间只有一个(或指定数量的)线程能访问共享资源,避免数据错乱。

C++11及以后的标准库(<mutex> 头文件)提供了多种锁,下面按"从简单到复杂"的顺序讲解,重点讲基础且常用的类型。


一、最基础的锁:std::mutex(互斥锁)

1. 作用

最核心、最基础的独占锁:同一时间只能有一个线程锁定它,其他线程尝试锁定会阻塞(一直等),直到锁被释放。

2. 核心用法

  • lock():加锁(如果锁已被占用,当前线程阻塞)
  • unlock():解锁(必须和lock配对,否则会导致死锁/资源泄漏)

3. 适用场景

最简单的独占式访问共享资源(比如单线程写、其他线程等),但不推荐手动用lock/unlock(容易忘unlock)。

4. 代码示例(基础用法,仅演示原理)

cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>

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

// 线程函数:累加count
void add_count() {
    for (int i = 0; i < 10000; ++i) {
        // 加锁:同一时间只有一个线程能执行下面的代码
        mtx.lock();
        count++;  // 临界区(访问共享资源的代码)
        // 解锁:必须手动解锁,否则其他线程永远拿不到锁
        mtx.unlock();
    }
}

int main() {
    std::thread t1(add_count);
    std::thread t2(add_count);
    
    t1.join();
    t2.join();
    std::cout << "最终count值:" << count << std::endl;  // 正确结果应该是20000
    return 0;
}

⚠️ 注意:如果代码在lock后抛出异常,unlock不会执行,会导致死锁!所以实际开发不用这种方式。


二、最安全的锁:std::lock_guard(RAII封装的mutex)

1. 作用

基于"RAII(资源获取即初始化)"思想,自动加锁、自动解锁 :创建lock_guard对象时自动调用lock(),对象销毁(比如出作用域)时自动调用unlock(),彻底避免忘解锁的问题。

2. 核心用法

直接创建lock_guard对象,传入mutex即可,无需手动调用lock/unlock。

3. 适用场景

90%的简单独占访问场景(比如单次读写共享资源),是实际开发中最常用的锁。

4. 代码示例(替代手动lock/unlock)

cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>

int count = 0;
std::mutex mtx;

void add_count() {
    for (int i = 0; i < 10000; ++i) {
        // 创建lock_guard,自动加锁
        std::lock_guard<std::mutex> lock(mtx);
        count++;  // 临界区
        // 出作用域时,lock对象销毁,自动解锁(即使count++抛异常也会解锁)
    }
}

int main() {
    std::thread t1(add_count);
    std::thread t2(add_count);
    
    t1.join();
    t2.join();
    std::cout << "最终count值:" << count << std::endl;  // 20000
    return 0;
}

三、更灵活的锁:std::unique_lock

1. 作用

lock_guard灵活:可以手动控制加锁/解锁时机 、支持超时加锁、可以配合条件变量(std::condition_variable)使用。

2. 核心用法

  • 构造时可选是否立即加锁:std::unique_lock<std::mutex> lock(mtx, std::defer_lock);(defer_lock表示延迟加锁)
  • 手动加锁:lock.lock()
  • 手动解锁:lock.unlock()

3. 适用场景

  • 需要在临界区中间临时解锁(比如锁内要调用耗时的无锁函数)
  • 配合条件变量(多线程通信的核心场景)
  • 需要超时尝试加锁(后续讲timed_mutex时会结合)

4. 代码示例(灵活解锁)

cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>

int count = 0;
std::mutex mtx;

void add_count() {
    for (int i = 0; i < 10000; ++i) {
        // 延迟加锁(构造时不加锁)
        std::unique_lock<std::mutex> lock(mtx, std::defer_lock);
        // 手动加锁
        lock.lock();
        count++;
        // 临时解锁(比如要执行耗时操作,不占用锁)
        lock.unlock();
        
        // 模拟耗时操作(无需锁的逻辑)
        std::this_thread::sleep_for(std::chrono::microseconds(1));
        
        // 再次加锁(如果需要)
        lock.lock();
        count++;
        // 出作用域自动解锁
    }
}

int main() {
    std::thread t1(add_count);
    std::thread t2(add_count);
    
    t1.join();
    t2.join();
    std::cout << "最终count值:" << count << std::endl;  // 40000
    return 0;
}

四、递归场景用:std::recursive_mutex(递归互斥锁)

1. 作用

允许同一个线程多次锁定同一个锁 (普通mutex如果同一个线程多次lock会直接死锁)。

2. 核心用法

mutex完全一致,但解锁次数必须和加锁次数相同。

3. 适用场景

  • 函数递归调用时需要加锁(比如递归遍历树,要保护共享的节点计数)
  • 同一个线程可能多次获取同一个锁的场景(尽量少用,大概率是代码设计有问题)

4. 代码示例(递归加锁)

cpp 复制代码
#include <iostream>
#include <mutex>

std::recursive_mutex rmtx;
int sum = 0;

// 递归函数:计算1~n的和
void recursive_add(int n) {
    // 加锁(同一线程多次调用也不会死锁)
    rmtx.lock();
    if (n == 0) {
        rmtx.unlock();  // 递归出口,解锁
        return;
    }
    sum += n;
    recursive_add(n - 1);  // 递归调用,再次加锁
    rmtx.unlock();  // 解锁次数和加锁次数一致
}

int main() {
    recursive_add(10);
    std::cout << "1~10的和:" << sum << std::endl;  // 55
    return 0;
}

五、不想等太久:std::timed_mutex(带超时的互斥锁)

1. 作用

尝试加锁时可以设置超时时间 ,超时后不再阻塞,返回false(普通mutex会无限期等)。

2. 核心用法

  • try_lock_for(时间段):尝试加锁,超时返回false(比如等1秒)
  • try_lock_until(时间点):尝试加锁到指定时间点,超时返回false

3. 适用场景

不想让线程无限期等待锁(比如非阻塞式访问资源,超时后可以做其他逻辑,比如提示"资源忙")。

4. 代码示例(超时加锁)

cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>

std::timed_mutex tmtx;

void try_lock_with_timeout() {
    // 尝试加锁,最多等1秒
    if (tmtx.try_lock_for(std::chrono::seconds(1))) {
        std::cout << "线程" << std::this_thread::get_id() << "获取锁成功!" << std::endl;
        std::this_thread::sleep_for(std::chrono::seconds(2));  // 占用锁2秒
        tmtx.unlock();
    } else {
        std::cout << "线程" << std::this_thread::get_id() << "获取锁超时!" << std::endl;
    }
}

int main() {
    std::thread t1(try_lock_with_timeout);
    std::thread t2(try_lock_with_timeout);
    
    t1.join();
    t2.join();
    return 0;
}

输出(t1拿到锁,t2等1秒超时):

复制代码
线程140709289268928获取锁成功!
线程140709280876224获取锁超时!

六、读多写少用:std::shared_mutex(共享互斥锁/C++17)

1. 作用

区分"读锁"和"写锁",提升并发效率:

  • 读锁(共享锁):多个线程可以同时加读锁(读-读不互斥)
  • 写锁(独占锁):只有一个线程能加写锁(读-写、写-写互斥)

2. 核心用法

  • 读锁:std::shared_lock<std::shared_mutex>
  • 写锁:std::unique_lock<std::shared_mutex>

3. 适用场景

读多写少的场景(比如配置文件读取、缓存查询、日志读取),比普通mutex并发更高。

4. 代码示例(读写分离)

cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>
#include <shared_mutex>  // C++17及以上

// 共享资源:模拟配置信息
std::string config = "初始配置";
// 共享互斥锁
std::shared_mutex smtx;

// 读配置(多线程同时读)
void read_config(int id) {
    // 加读锁(共享锁)
    std::shared_lock<std::shared_mutex> lock(smtx);
    std::cout << "读者" << id << "读取配置:" << config << std::endl;
    std::this_thread::sleep_for(std::chrono::milliseconds(100));  // 模拟读耗时
}

// 写配置(独占)
void write_config(const std::string& new_config) {
    // 加写锁(独占锁)
    std::unique_lock<std::shared_mutex> lock(smtx);
    std::cout << "写者修改配置:" << new_config << std::endl;
    config = new_config;
    std::this_thread::sleep_for(std::chrono::milliseconds(200));  // 模拟写耗时
}

int main() {
    // 5个读者线程 + 1个写者线程
    std::thread t1(read_config, 1);
    std::thread t2(read_config, 2);
    std::thread t3(write_config, "新配置1");
    std::thread t4(read_config, 3);
    std::thread t5(read_config, 4);
    
    t1.join();
    t2.join();
    t3.join();
    t4.join();
    t5.join();
    return 0;
}

输出(读者1、2同时读,写者执行时读者3、4等待,写完成后读者3、4读新配置):

复制代码
读者1读取配置:初始配置
读者2读取配置:初始配置
写者修改配置:新配置1
读者3读取配置:新配置1
读者4读取配置:新配置1

总结:核心知识点回顾

  1. 基础首选std::lock_guard(RAII自动管理,简单安全,90%场景够用),底层依赖std::mutex
  2. 灵活场景std::unique_lock(手动控制锁的生命周期、配合条件变量)。
  3. 特殊场景
    • 递归调用加锁:std::recursive_mutex(尽量少用);
    • 不想无限等锁:std::timed_mutex(超时加锁);
    • 读多写少:std::shared_mutex(C++17,提升读并发)。
  4. 核心原则:永远用RAII方式(lock_guard/unique_lock),不要手动lock/unlock,避免死锁。

新手入门先掌握std::mutex+std::lock_guard即可,这是最基础、最常用的组合,后续再逐步学习unique_lock和共享锁。

相关推荐
坐怀不乱杯魂2 小时前
Linux网络 - Socket编程(IPv4&IPv6)
linux·服务器·网络·c++·udp·tcp
Yupureki2 小时前
《算法竞赛从入门到国奖》算法基础:搜索-BFS初识
c语言·数据结构·c++·算法·visual studio·宽度优先
CSDN_RTKLIB4 小时前
两版本锁抛出异常测试
c++
晨非辰4 小时前
Linux权限管理速成:umask掩码/file透视/粘滞位防护15分钟精通,掌握权限减法与安全协作模型
linux·运维·服务器·c++·人工智能·后端
u01092727112 小时前
C++中的策略模式变体
开发语言·c++·算法
Aevget13 小时前
MFC扩展库BCGControlBar Pro v37.2新版亮点:控件功能进一步升级
c++·mfc·界面控件
Tansmjs14 小时前
C++与GPU计算(CUDA)
开发语言·c++·算法
挖矿大亨15 小时前
c++中的函数模版
java·c++·算法
阿基米东15 小时前
基于 C++ 的机器人软件框架(具身智能)开源通信库选型分析
c++·机器人·开源