C++(标准库):48---并发之(互斥体mutex、lock_guard、recursive_mutex、timed_mutex、recursive_timed_mutex、unique_lock)

本文详细解析了C++中mutex、lock_guard、unique_lock等互斥锁的概念及应用,包括基本使用、递归锁、带时间性的锁、多锁处理以及unique_lock的高级特性。

一、mutex

  • mutex全名mutual exclusion(互斥体),是个object,用来协助采取独占排他方式控制“对资源的并发访问”
  • 例如,下面对一份资源进行锁定
void f(int val);

int val;             //共享资源
std::mutex valMutex; //互斥体

void func()
{
    //锁定,然后操作共享资源
    valMutex.lock();
    if (val >= 0)
        f(val);
    else
        f(-val);
    //访问完之后解锁
    valMutex.unlock();
}

二、lock_guard

  • lock_guard是采用RAII手法封装的一个类,功能与mutex一样
  • 其在构造时自动对mutex进行锁定(lock),在析构时,在析构函数中自动对mutex进行解锁(unlock)
  • 其比mutex的好处:
    • 使用mutex,我们需要自己进行加锁(lock)和解锁(unlock)。如果对mutex进行了加锁,但是当资源访问完之后却没有对mutex进行解锁,那么其他访问这份共享资源的方法就会永远阻塞
    • lock_guard的优点时在构造时自动对mutex加锁,在作用域结束/析构时,自动对mutex进行解锁
  • 例如:
void f(int val);

int val;
std::mutex valMutex; //互斥体

int main()
{
    //以mutex声明一个lock_guard,其在构造时自动对传入的mutex进行lock
    std::lock_guard<std::mutex> lg(valMutex);
    if (val >= 0)
        f(val);
    else
        f(-val);
}//作用域结束后,lg进行析构,在析构函数中,其自动对mutex进行unlock

三、mutex和lock_guard的第一个演示案例

#include <iostream>
#include <thread>
#include <future>
#include <string>
using namespace std;

std::mutex printMutex;

void print(const std::string& s)
{
    //锁住mutex,保证每个线程打印时不会有别的线程也在打印
    std::lock_guard<std::mutex> l(printMutex);
    for (char c : s) 
    {
        std::cout.put(c);
    }
    std::cout << std::endl;
}//作用域结束,mutex自动释放

int main()
{
    auto f1 = std::async(std::launch::async, print, "Hello from a first thread");
    auto f2 = std::async(std::launch::async, print, "Hello from a second thread");

    print("Hello from the main thread");
}//因为async()的发射策略为std::launch::async,即使我们没有使用future.get()获得他们的结果,但是main()一定会等待两个线程执行结束才结束
  • 显示结果如下:

  • 如果在print中没有使用mutex,那么显示的结果可能为:

四、递归锁(recursive_mutex)

  • 在有时候,递归锁定是必要的,典型例子就是active object或monitor,它们是在每个public函数内放一个mutex并取得其lock,用以放置data race腐蚀对象的内部状态

普通的锁不能正常进行递归锁定(死锁)

  • 例如,下面是一个数据库类及其接口,每个接口中都会锁定mutex成员
class DatabaseAccess 
{
public:
    void createTable()
    {
        std::lock_guard<std::mutex> lg(dbMutex);
        //...
    }
    void insertData()
    {
        std::lock_guard<std::mutex> lg(dbMutex);
        //...
    }
    //这个接口中,间接调用了createTable()
    void createTableAndInsertData()
    {
        std::lock_guard<std::mutex> lg(dbMutex);
        //...
        createTable();
    }
private:
    std::mutex dbMutex;
};
  • 但是上面的createTableAndInsertData()接口会造成deadlock(死锁),因为其在内部对mutex进行加锁,之后又调用了createTable()接口,createTable()接口也会对mutex进行加锁,但是由于mutex已经被加锁了,因此createTable()发生死锁
  • 如果平台侦测处类似上述的deadlock,C++标准库允许第二次lock抛出异常std::system_error并带有差错码resource_deadlock_would_occur但并非必然而且情况往往不是如此

递归所(recursive_mutex)

  • 借助recursive_mutex,上述的行为就不会有问题了。recursive_mutex允许同一线程多次锁定,并在最近一次相应的unlock时释放lock
  • 例如,我们修改上面的DatabaseAccess类及其接口:
class DatabaseAccess 
{
public:
    void createTable()
    {
        std::lock_guard<std::recursive_mutex> lg(dbMutex);
        //...
    }
    void insertData()
    {
        std::lock_guard<std::recursive_mutex> lg(dbMutex);
        //...
    }
    void createTableAndInsertData()
    {
        std::lock_guard<std::recursive_mutex> lg(dbMutex);
        //...
        createTable();
    }
private:
    std::recursive_mutex dbMutex;
};

五、mutex的成员函数:尝试性的lock(try_lock())

  • try_lock()成员函数的作用是:对mutex进行锁定,如果能锁定就返回true,如果不能锁定就不阻塞直接返回false
std::mutex m;

//对m进行尝试性加锁,加锁成功才结束while
while (m.try_lock() == false)
{
    doSomeOtherStuff();
}

//...

//使用完解锁
m.unlock();
  • 如果lock_guard想要使用try_lock()加的锁,需要传递一个额外实参adopt_lock给其构造函数。例如:
std::mutex m;

//对m进行尝试性加锁,加锁成功才结束while
while (m.try_lock() == false)
{
    doSomeOtherStuff();
}
//加锁完成之后,将mutex交给lock_guard<>进行管理,此时需要传入std::adopt_lock参数
std::lock_guard<std::mutex> lg(m, std::adopt_lock);
  • 注意:try_lock()有可能假性失败,也就是说即使lock并未被他人使用也可能失败返回false(之所以提供这样的行为是为了memory-ordering(内存处理次序),但是这个并不广为人知)

六、带时间性的lock(timed_mutex、recursive_timed_mutex)

  • 为了等待特定的时间再进行加锁,那么可以使用所谓timed mutex
  • 标准库提供了std::timed_mutex和std::recursive_timed_mutex。并且这两个类都提供了try_lock_for()和try_lock_until(),用以等待某个时间段,或直至到达某个时间点再进行加锁
  • 例如:
std::timed_mutex m;

//尝试加锁1秒钟,如果在1秒钟加锁成功,就执行if
if (m.try_lock_for(std::chrono::seconds(1))) 
{
    //将timed_mutex交给lock_guard进行管理,注意其构造函数需要传入std::adopt_lock
    std::lock_guard<std::timed_mutex> lg(m, std::adopt_lock);
}
else 
{
    couldNotGetTheLock();
}

七、处理多个lock(全局std::lock()函数)

  • 有时候,一次需要锁定多个mutex(例如为了传送数据,从一个受保护资源到另一个受保护的资源,需要将两份资源同时锁定)
  • 如果使用前面的lock机制,那么可能会发生复杂且具有风险:例如你取得第一个lock却拿不到第二个lock,或许发生deadlock(如果以不同的次序去锁住相同的lock)

全局std::lock()函数

  • 全局std::lock()函数允许你一次锁定多个mutex
  • 例如:
std::mutex m1;
std::mutex m2;

void func()
{
    std::lock(m1, m2);
    std::lock_guard<std::mutex> lockM1(m1, std::adopt_lock);
    std::lock_guard<std::mutex> lockM2(m2, std::adopt_lock);
    //...
}//作用域结束后,m1,m2都自动释放
  • std::lock()的注意事项:
    • 该函数会锁住它收到的所有mutex,并且阻塞到所有的mutex都被锁定或直到抛出异常才返回
    • 如果锁定的过程中抛出了异常,那么已经加锁的mutex会被释放
    • 在锁定之后,你可以配合lock_guard对mutex进行使用,并且需要传递std::adopt_lock给lock_guard的构造函数
    • 这个lock()函数提供了一个deadlock回避机制,但是多个lock的锁定次序并不明确

全局std::try_lock()函数

  • 该函数也可以对多个mutex进行加锁,但是其实进行尝试性加锁的。其工作原理与返回值密切相关,详情见下面的返回值
  • 返回值:
    • 如果对所有的mutex都加锁成功,返回-1。此时可以对所有的mutex进行操作了
    • 如果对其中的一部分锁没有加锁成功,那么返回第一个失败的lock的索引(从0开始)。此时已经成功加锁的mutex会被释放
  • 例如:
std::mutex m1;
std::mutex m2;

void func()
{
    int idx = std::try_lock(m1, m2);
    //如果对m1和m2都加锁成功,返回-1,执行if
    if (idx < 0) 
    {
        std::lock_guard<std::mutex> lockM1(m1, std::adopt_lock);
        std::lock_guard<std::mutex> lockM2(m2, std::adopt_lock);
    }
    else
    {
        std::cerr << "could not lock mutex m" << idx + 1 << std::endl;
    }
}
  • 注意:这个try_lock()不提供deadlock会比机制,但它保证以出现于try_lock()实参列的次序来对mutex进行尝试性加锁
  • 注意:使用lock()函数或try_lock()函数对mutex进行加锁之后,你仍然需要在作用域结束之后对mutex进行解锁(unlock()),但是一般我们会将其传递给lock_guard<>使用

八、unique_lock

  • 标准库还提供了一个unique_lock<>类,它对付mutex更有弹性
  • unique_lock<>的接口和lock_guard<>的接口相同,但是允许写出“何时加锁”以及“如何锁定或解锁”其mutex。此外,unique_lock还提供了owns_lock()和bool()接口来查询其mutex目前是否被锁住

三种锁定标志

  • 第一种:你可以传递try_to_lock,表示尝试性对mutex进行加锁,如果加锁成功就返回true,如果加锁失败就不阻塞而直接返回false。代码如下:
std::mutex m;

void func()
{
    //尝试加锁m,但是不阻塞
    std::unique_lock<std::mutex> lock(m, std::try_to_lock);
    //如果加锁成功,执行if
    if (lock)
    {
        //...
    }
}
//作用域结束后,如果unique_lock对m成功加锁,那么unique_lock的析构函数释放m;如果没有,其析构函数什么都不做
  • 第二种:你可以传递一个时间段或时间点给构造函数,表示尝试在一个明确的时间上对mutex进行加锁
std::mutex m;

void func()
{
    //在1秒钟之后,对m进行加锁,如果加锁成功就返回,加锁失败就阻塞等待
    std::unique_lock<std::mutex> lock(m, std::chrono::seconds(1));
    //...
}
//作用域结束后,如果unique_lock对m成功加锁,那么unique_lock的析构函数释放m;如果没有,其析构函数什么都不做
  • 第三种:你可以传递defer_lock给其构造函数,表示初始化unique_lock,但是并不加锁,而是在后面自己调用lock()成员函数进行加锁
std::mutex m;

void func()
{
    //只是单单的用m初始化lock,但是不进行加锁
    std::unique_lock<std::mutex> lock(m, std::defer_lock);
    //...
    lock.lock(); //调用此成员函数,对m进行加锁
    //...
}
//作用域结束后,如果unique_lock对m成功加锁,那么unique_lock的析构函数释放m(不需要调用unlock);如果没有,其析构函数什么都不做

std:::defer_lock标志

  • 上述的第三种使用方式中的std:::defer_lock标志,可以用来建立一或多个lock并于稍后才锁住它们
  • 例如:
std::mutex m1;
std::mutex m2;
std::mutex m3;

void func()
{
    std::unique_lock<std::mutex> lockM1(m1, std::defer_lock);
    std::unique_lock<std::mutex> lockM2(m2, std::defer_lock);
    std::unique_lock<std::mutex> lockM3(m3, std::defer_lock);
    //...
    std::lock(m1, m2, m3);
}
  • 如果没有任何锁定标志,那么unique_lock与lock_guard一样,直接对mutex进行锁定
std::mutex m;

void func()
{
    //与lock_guard<>一样,尝试对m进行加锁,加锁成功就返回;加锁失败就阻塞
    std::unique_lock<std::mutex> lock(m);
}
  • 此外,unique_lock还提供了release()用来释放mutex,或是将其mutex拥有权转移给另一个lock。详细的成员函数见下面“九”中的介绍

演示案例

  • 有了lock_guard和unique_lock作为工具,现在我们可以实现一个例子,以轮询某个ready flag的方式,令一个线程等待另一个线程
  • 代码如下:
bool readyFlag;
std::mutex readyFlagMutex;

void thread1()
{
    //做一些thread2需要的准备工作
    //...
    std::lock_guard<std::mutex> lg(readyFlagMutex);
    readyFlag = true;
}

void thread2()
{
    //等待readyFlag变为true
    {
        std::unique_lock<std::mutex> ul(readyFlagMutex);
        //如果readyFlag仍未false,说明thread1还没有锁定,那么持续等待
        while (!readyFlag)
        {
            ul.unlock();
            std::this_thread::yield();
            std:this_thread::sleep_for(std::chrono::milliseconds(100));
            ul.lock();
        }
    }//释放lock

    //在thread1锁定之后,做相应的事情
}

九、细说mutex和lock

细说mutex

  • 标准库提供了四个mutex,如下:

  • 比较如下:

  • 下面列出了mutex的操作函数:

  • lock()成员函数有可能抛出std::system_error并带有下面的差错码:
    • operation_not_permitted——如果线程的特权级(privilege)不足以执行次操作
    • resource_deadlock_would_occur——如果平台侦测到有个deadlock即将发生
    • device_or_resource_busy——如果mutex已被锁定而无法形成阻塞(blocking)
  • 如果程序解除(unlock)一个并非它所拥有的mutex object,或是销毁一个被任何线程拥有的mutex object,或是线程拥有mutex object但却结束了生命,将导致不明确的行为
  • 注意,处理系统时间调整时,try_lock_for()和try_lock_until()通常有异

细说lock_guard

  • 下图列出了lock_guard的操作函数

细说unique_lock

  • unique_lock为一个不一定得锁住的mutex提供一个lock guard。它提供的接口如下图所示:

  • lock()可能抛出std::system_error,其所夹带的差错码和mutex的lock()所引发的相同
  • unlock()可能抛出std::system_error并夹带差错码operation_not_permitted——如果这个unique lock并未被锁的话
<think>我们讨论的是在Linux C++开发中使用`std::lock_guard<std::mutex>`是否会导致死锁。 首先,需要明确死锁是什么?死锁是指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。 其次,`std::lock_guard`是一个RAII(资源获取即初始化)类,用于管理互斥锁(`std::mutex`)。它在构造时锁定互斥锁,在析构时(通常是离开其作用域时)自动释放互斥锁。这种方式避免了忘记解锁的情况,减少了死锁的可能性。 但是,使用`std::lock_guard`并不能完全避免死锁。下面是几种可能导致死锁的情况: 1. **多个锁的获取顺序不一致**:如果多个线程以不同的顺序获取多个锁,则可能导致死锁。例如: 线程1:先获取锁A,再获取锁B; 线程2:先获取锁B,再获取锁A; 那么当线程1持有锁A等待锁B,而线程2持有锁B等待锁A时,就会发生死锁。 2. **递归使用同一个锁**:同一个线程多次获取同一个非递归互斥锁(`std::mutex`不是递归锁)会导致死锁。因为第一次获取锁后,第二次再尝试获取同一个锁时就会阻塞,而该锁已经被自己持有,导致永远等待。例如: ```cpp std::mutex mtx; void func() { std::lock_guard<std::mutex> lock(mtx); // 第一次获取锁 another_func(); // 内部又尝试获取同一个锁 } void another_func() { std::lock_guard<std::mutex> lock(mtx); // 第二次尝试获取同一个锁,死锁! } ``` 3. **在持有锁的情况下等待另一个线程完成**:例如,线程1持有锁A,然后等待线程2完成;而线程2正在尝试获取锁A才能完成,这就形成了死锁。 4. **异常导致锁未被释放**:但注意,`std::lock_guard`在构造成功后,即使后续抛出异常,其析构函数也会被调用从而释放锁。因此,在锁的范围内发生异常不会导致死锁(因为锁会被释放),但可能导致数据不一致。所以,这一条通常不会导致死锁,因为RAII机制保证了锁的释放。 所以,回答用户的问题:使用`std::lock_guard`本身不会导致死锁,而且它通过自动释放锁减少了死锁的可能性。但是,如果程序员错误地使用了锁(例如多个锁的顺序不一致),仍然可能导致死锁。 因此,在使用`std::lock_guard`时,应注意: - 避免嵌套使用多个锁,如果必须使用多个锁,确保所有线程以相同的顺序获取锁。 - 不要在同一线程中多次获取同一个非递归互斥锁(如果需要递归锁,请使用`std::recursive_mutex`)。 - 尽量减少锁的持有时间,仅锁住必要的代码区域。 下面是一个死锁的例子(多个锁顺序不一致): ```cpp #include <mutex> #include <thread> std::mutex mtx1; std::mutex mtx2; void thread1() { std::lock_guard<std::mutex> lock1(mtx1); // 先锁mtx1 std::this_thread::sleep_for(std::chrono::milliseconds(100)); std::lock_guard<std::mutex> lock2(mtx2); // 再锁mtx2 } void thread2() { std::lock_guard<std::mutex> lock2(mtx2); // 先锁mtx2 std::this_thread::sleep_for(std::chrono::milliseconds(100)); std::lock_guard<std::mutex> lock1(mtx1); // 再锁mtx1 } int main() { std::thread t1(thread1); std::thread t2(thread2); t1.join(); t2.join(); return 0; } ``` 上述代码中,两个线程以相反的顺序获取锁,可能导致死锁。 总结:`std::lock_guard`本身的设计是为了避免忘记解锁而造成死锁,但它不能解决所有死锁问题,特别是由于锁的获取顺序不当引起的死锁。因此,程序员需要谨慎设计锁的获取顺序。 引用说明:`std::lock_guard`在离开作用域时会自动释放锁,这避免了手动解锁可能导致的遗漏问题[^3]。 生成相关问题: 1. 如何避免多线程中多个锁的顺序问题? 2. 在C++中,递归锁(`std::recursive_mutex`)的使用场景和注意事项是什么? 3. 如何调试多线程程序中的死锁问题? 4. 除了`std::lock_guard`,C++还提供了哪些锁管理工具?它们分别适用于什么场景? </think>在 Linux C++ 开发中,正确使用`std::lock_guard<std::mutex>` **不会导致死锁**,但**错误的使用方式可能引发死锁**。以下是关键分析: ### ✅ 安全使用场景(不会死锁) ```cpp std::mutex mtx; int shared_data = 0; void safe_write() { std::lock_guard<std::mutex> lock(mtx); // 构造时自动加锁 shared_data = 42; // 操作共享数据 } // 作用域结束,自动解锁 ✅ ``` **安全原理**: 1. RAII 机制确保锁在作用域结束时**必然释放**,即使发生异常[^3] 2. 单锁操作不存在锁顺序问题 3. 无需手动调用`lock()`/`unlock()`,避免疏忽遗漏 ### ❌ 导致死锁的常见错误 1. **嵌套锁定同一把锁** ```cpp void deadlock_example() { std::lock_guard<std::mutex> lock1(mtx); // 第一次加锁 { std::lock_guard<std::mutex> lock2(mtx); // 尝试重入 ❌ // 阻塞在此处 -> 死锁! } } ``` *原因*:非递归互斥(`std::mutex`)不支持重入[^3] 2. **多锁顺序不一致** ```cpp std::mutex mtxA, mtxB; void thread1() { std::lock_guard<std::mutex> lockA(mtxA); // 顺序 A->B std::lock_guard<std::mutex> lockB(mtxB); } void thread2() { std::lock_guard<std::mutex> lockB(mtxB); // 顺序 B->A ❌ std::lock_guard<std::mutex> lockA(mtxA); // 死锁点! } ``` ### 🔒 死锁避免方案 | 场景 | 解决方案 | 代码示例 | |------|----------|----------| | 递归锁定需求 | 改用递归锁 | `std::recursive_mutex rmtx;` | | 多锁操作 | 统一加锁顺序 | 约定所有线程先锁 A 再锁 B | | 复杂锁管理 | 使用`std::scoped_lock` | `std::scoped_lock lock(mtxA, mtxB);` | | 条件等待 | 配合`std::condition_variable` | `cv.wait(lock, []{return ready;});` | ### 💡 最佳实践指南 1. **锁粒度最小化**:锁只覆盖临界区代码 ```cpp void efficient_fn() { // ...非临界区代码... { std::lock_guard lock(mtx); // 精细作用域 critical_operation(); } // ...其他代码... } ``` 2. **避免跨函数持锁**: ```cpp void unsafe_fn() { std::lock_guard lock(mtx); call_external_function(); // 危险!可能嵌套锁或死锁 } ``` 3. **锁超时机制**(应急方案): ```cpp std::timed_mutex tmtx; if(tmtx.try_lock_for(100ms)) { // 非阻塞尝试 std::lock_guard lock(tmtx, std::adopt_lock); // ... } ``` > **关键结论**:`std::lock_guard` 本身是**死锁安全的RAII封装**,但死锁风险源自**程序员对锁规则的违反**(如重入、顺序错乱)[^3][^4]。结合单锁作用域管理和统一锁顺序可彻底避免死锁。 --- ### 📚 相关问题 1. `std::lock_guard` 和 `std::unique_lock` 在死锁预防上有何区别? 2. 如何调试 C++ 多线程程序中的死锁问题? 3. 递归互斥(`std::recursive_mutex`) 的使用场景和性能代价是什么? 4. 在生产者-消费者模型中如何避免条件变量导致的死锁?
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

董哥的黑板报

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值