两版本锁抛出异常测试

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

文章目录


你想测试手动调用 std::mutexlock()/unlock() 方法时,临界区抛出异常并捕获的场景,对比 std::lock_guard 看看手动加锁是否存在缺陷(比如 g_count 无法累加至预设值、甚至死锁),我会为你编写手动加锁 vs lock_guard 的对比示例,并详细解释核心问题。

核心思路

手动加锁的关键缺陷:如果临界区抛出异常,unlock() 语句若写在异常之后,会因异常中断执行而永远无法解锁 ,导致互斥锁被永久占用(死锁),其他线程阻塞,最终 g_count 既达不到预设值,程序还可能卡死;而 lock_guard 基于 RAII 机制,即使异常也会触发析构函数自动解锁,不会出现这类问题。


完整对比示例代码

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

int g_count = 0;
std::mutex g_mutex;

// ========== 手动加锁解锁(有缺陷版本) ==========
void incrementManual(int times) {
    try {
        for (int i = 0; i < times; ++i) {
            // 手动加锁
            g_mutex.lock();

            // 模拟临界区异常(第500次循环触发)
            if (i == 500) {
                std::cout << "手动加锁线程:触发异常,unlock() 将无法执行!\n";
                throw std::runtime_error("手动加锁:临界区异常");
            }

            g_count++;
            // 手动解锁(⚠️ 异常会跳过这行,锁永远无法释放)
            g_mutex.unlock();
        }
    } catch (const std::exception& e) {
        std::cout << "手动加锁线程捕获异常:" << e.what() << "\n";
        // 【修复提示】手动加锁必须在这里手动解锁,否则死锁,但新手极易遗漏
        // g_mutex.unlock(); // 若注释掉这行,必然死锁;解开则能解锁,但代码易出错
    }
}

// ========== lock_guard 加锁(安全版本) ==========
void incrementGuard(int times) {
    try {
        for (int i = 0; i < times; ++i) {
            // RAII 加锁:构造时lock,析构时自动unlock(无论是否异常)
            std::lock_guard<std::mutex> lock(g_mutex);

            // 同样模拟临界区异常
            if (i == 500) {
                std::cout << "lock_guard线程:触发异常,析构会自动解锁!\n";
                throw std::runtime_error("lock_guard:临界区异常");
            }

            g_count++;
        }
    } catch (const std::exception& e) {
        // 仅捕获异常,无需手动解锁(lock_guard已自动处理)
        std::lock_guard<std::mutex> lock(g_mutex); // 加锁保证输出不乱码
        std::cout << "lock_guard线程捕获异常:" << e.what() << "\n";
    }
}

int main() {
    const int thread_num = 5;
    const int times_per_thread = 1000;
    std::vector<std::thread> threads;

    // ========== 测试1:手动加锁(会死锁/计数异常) ==========
    std::cout << "===== 测试手动加锁(未手动解锁异常)=====\n";
    g_count = 0; // 重置计数
    for (int i = 0; i < thread_num; ++i) {
        threads.emplace_back(incrementManual, times_per_thread);
    }
    // 等待线程执行(⚠️ 这里会卡死,因为锁被永久占用)
    for (auto& t : threads) {
        if (t.joinable()) {
            t.join(); // 程序会卡在这一行,无法继续
        }
    }
    std::cout << "手动加锁最终count值:" << g_count << "\n"; // 这行永远执行不到

    // ========== 测试2:lock_guard 加锁(正常执行) ==========
    // 【注意】先注释掉测试1的代码,再运行测试2,否则测试1卡死无法执行到这里
    // std::cout << "\n===== 测试lock_guard加锁(安全)=====\n";
    // g_count = 0;
    // threads.clear();
    // for (int i = 0; i < thread_num; ++i) {
    //     threads.emplace_back(incrementGuard, times_per_thread);
    // }
    // for (auto& t : threads) {
    //     if (t.joinable()) {
    //         t.join();
    //     }
    // }
    // std::cout << "lock_guard最终count值:" << g_count << "\n"; // 输出 2500

    return 0;
}

关键运行结果与解释

1. 手动加锁版本(测试1)的问题
  • 现象 :程序运行后会输出「手动加锁线程:触发异常,unlock() 将无法执行!」,然后卡死在 t.join()g_count 的最终值永远打印不出来;
  • 核心原因
    • 线程执行到 i=500 时抛出异常,跳过了后续的 g_mutex.unlock()
    • 互斥锁被该线程永久持有,其他线程调用 g_mutex.lock() 时会阻塞(拿不到锁),最终所有线程卡死,g_count 通常远低于 2500(甚至只有几百);
    • 即使在 catch 块中手动加 g_mutex.unlock()(解开代码中注释),也能解锁,但新手极易遗漏这行代码,且代码逻辑会变得繁琐(每个异常分支都要考虑解锁)。
2. lock_guard 版本(测试2)的正常表现
  • 现象 :程序输出「lock_guard线程:触发异常,析构会自动解锁!」,无卡死,最终 g_count 输出 2500(5个线程×每个执行500次);
  • 核心原因
    • std::lock_guard 是 RAII 封装:构造时调用 lock()无论正常执行还是异常退出作用域 ,析构函数都会自动调用 unlock()
    • 即使抛出异常,锁也能被释放,其他线程可正常竞争锁,g_count 能累加至预期值(每个线程执行到500次抛异常,总计 5×500=2500)。

总结

  1. 手动加锁的核心缺陷 :异常会跳过 unlock() 语句,导致锁永久占用(死锁),g_count 累加异常;即使手动在 catch 中解锁,代码也易出错、可读性差;
  2. lock_guard 的核心优势:RAII 机制保证「加锁-解锁」成对出现,无论临界区正常执行还是抛出异常,都能自动释放锁,避免死锁,计数结果符合预期;
  3. 实战建议 :除非有特殊需求(如手动控制解锁时机),否则优先使用 std::lock_guard(或 std::unique_lock),避免手动调用 lock()/unlock()

如果想测试「手动加锁但在catch中解锁」的修复版本,只需解开 incrementManualcatch 块里的 g_mutex.unlock(); 注释,此时程序不会死锁,g_count 也能输出 2500,但代码复杂度远高于 lock_guard 版本。

相关推荐
晨非辰1 小时前
Linux权限管理速成:umask掩码/file透视/粘滞位防护15分钟精通,掌握权限减法与安全协作模型
linux·运维·服务器·c++·人工智能·后端
u01092727110 小时前
C++中的策略模式变体
开发语言·c++·算法
Aevget11 小时前
MFC扩展库BCGControlBar Pro v37.2新版亮点:控件功能进一步升级
c++·mfc·界面控件
Tansmjs11 小时前
C++与GPU计算(CUDA)
开发语言·c++·算法
挖矿大亨13 小时前
c++中的函数模版
java·c++·算法
阿基米东13 小时前
基于 C++ 的机器人软件框架(具身智能)开源通信库选型分析
c++·机器人·开源
偷星星的贼1113 小时前
C++中的对象池模式
开发语言·c++·算法
CN-Dust13 小时前
【C++】洛谷P3073 [USACO13FEB] Tractor S
开发语言·c++
2401_8290040213 小时前
C++中的适配器模式变体
开发语言·c++·算法