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


#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(); // 若注释掉这行,必然死锁;解开则能解锁,但代码易出错//但是测试发现结果是对的
    }
}


int main()
 {
    const int thread_num = 5;
    const int times_per_thread = 1000;
    std::vector<std::thread> threads;
    
    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"; // 这行永远执行不到
    return 0;
}

你解开 catch 块里的 g_mutex.unlock(); 注释后,代码能正常执行且结果正确并不是巧合,但这种「手动在 catch 里解锁」的做法仅在这个极简场景下可行,在工程实践中是脆弱且不推荐的——它依赖开发者对所有异常/退出路径的精准把控,稍有不慎就会出问题。

一、为什么解开注释后能正常运行?

核心逻辑是:
你在 try 块里调用了 g_mutex.lock() 加锁,抛出异常后进入 catch 块,此时手动调用 g_mutex.unlock() 补上了解锁操作,让互斥锁被释放。
这就实现了「加锁-解锁」的成对执行,因此不会死锁,其他线程能正常竞争锁,最终 g_count 也能累加至预期的 2500。

但这只是「刚好覆盖当前场景」,并非通用的安全做法。

二、手动解锁的隐藏风险(为什么不推荐)

下面用具体例子展示:哪怕只是轻微修改代码,手动解锁就会失效,而 lock_guard 始终安全。

1. 风险1:多个退出点导致解锁遗漏

如果函数里有提前 return多个异常分支,很容易漏掉某个路径的解锁:

void incrementManualRisk1(int times) {
    try {
        for (int i = 0; i < times; ++i) {
            g_mutex.lock();

            // 新增:提前退出分支(新手易忽略)
            if (i == 300) {
                std::cout << "提前return,跳过unlock!\n";
                return; // 直接return,既跳过正常unlock,也进不去catch
            }

            if (i == 500) {
                throw std::runtime_error("临界区异常");
            }

            g_count++;
            g_mutex.unlock();
        }
    } catch (const std::exception& e) {
        g_mutex.unlock(); // 仅覆盖异常场景,覆盖不了return场景
        std::cout << "捕获异常:" << e.what() << "\n";
    }
}
  • 现象:当线程执行到 i=300return,跳过了所有解锁逻辑,锁被永久占用,程序卡死;
  • lock_guard 对比:哪怕 returnlock_guard 也会在离开作用域时自动析构解锁,完全无风险。
2. 风险2:lock 后抛异常不在 try 块内

如果 lock 后、try 块前抛异常(比如参数非法),catch 块根本捕获不到,解锁代码执行不到:

void incrementManualRisk2(int times) {
    for (int i = 0; i < times; ++i) {
        g_mutex.lock();

        // 异常抛在try块外,catch捕获不到
        if (times < 0) {
            throw std::runtime_error("参数非法"); // 解锁代码永远执行不到
        }

        try {
            if (i == 500) {
                throw std::runtime_error("临界区异常");
            }
            g_count++;
            g_mutex.unlock();
        } catch (const std::exception& e) {
            g_mutex.unlock(); // 仅能捕获try块内的异常
            std::cout << "捕获异常:" << e.what() << "\n";
        }
    }
}
  • 现象:若传入 times=-1,异常抛在 try 块外,catch 无法处理,解锁代码执行不到,直接死锁;
  • 这里AI说错了,传-1连循环都进入不了,但类似的如果进入这种未捕获抛出会崩溃
  • lock_guard 对比:只要 lock_guard 构造完成,无论异常在哪抛,析构都会解锁;哪怕构造前抛异常(比如参数非法),lock_guard 没构造,也不会加锁,无风险。
3. 风险3:catch 块内再抛异常

如果 catch 块里处理逻辑又抛异常,手动解锁代码会被跳过:

void incrementManualRisk3(int times) {
    try {
        for (int i = 0; i < times; ++i) {
            g_mutex.lock();
            if (i == 500) {
                throw std::runtime_error("临界区异常");
            }
            g_count++;
            g_mutex.unlock();
        }
    } catch (const std::exception& e) {
        // 处理异常时再抛新异常,跳过unlock
        throw std::runtime_error("处理异常时出错:" + std::string(e.what()));
        g_mutex.unlock(); // 这行永远执行不到
    }
}
  • 现象:新异常抛出后,unlock 被跳过,锁无法释放,程序卡死;
  • lock_guard 对比:哪怕 catch 块抛新异常,lock_guard 早已在 try 块结束时析构解锁,无影响。

三、lock_guard 为什么是“真正可行”的方案?

std::lock_guard 的核心是 RAII(资源获取即初始化),它把「加锁-解锁」绑定到对象的「构造-析构」:

  • 构造 lock_guard 对象 → 自动调用 lock()
  • 无论正常退出作用域(比如循环结束)、异常退出(抛异常)、提前 return,只要对象离开作用域,析构函数就会自动调用 unlock()

这种“自动绑定”的方式,从根本上避免了“开发者手动把控所有路径”的人为失误,是 C++ 工程中处理互斥锁的标准最佳实践

总结

  1. 解开 catch 里的 unlock 能运行不是巧合:它补上了异常场景的解锁,实现了“加锁-解锁”成对,但仅适配当前极简场景;
  2. 手动解锁不可行:依赖开发者覆盖所有退出/异常路径,极易因代码修改、逻辑遗漏导致死锁;
  3. lock_guard工程上可靠的方案:RAII 机制自动保证解锁,无需关注异常/退出路径,代码更简洁、更健壮。

简单说:手动解锁是“靠人保证不出错”,lock_guard 是“靠语言机制保证不出错”——而工程开发中,机制永远比人的细心更可靠。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐