C++ unique_lock,共享锁和递归锁

简介

本文介绍C++ 并发中使用的其他类型的锁,包括unique_lockshared_lock, 以及recursive_lock等。shared_lockunique_lock比较常用,而recursive_lock用的不多,或尽可能规避用这种锁。

unique_lock

unique_locklock_guard基本用法相同,构造时默认加锁,析构时默认解锁,但unique_lock有个好处就是可以手动解锁。这一点尤为重要,方便我们控制锁住区域的粒度(加锁的范围大小),也能支持和条件变量配套使用,至于条件变量我们之后再介绍,本文主要介绍锁的相关操作。

  1. //unique_lock 基本用法
  2. std::mutex mtx;
  3. int shared_data = 0;
  4. void use_unique() {
  5. //lock可自动解锁,也可手动解锁
  6. std::unique_lock<std::mutex> lock(mtx);
  7. std::cout << "lock success" << std::endl;
  8. shared_data++;
  9. lock.unlock();
  10. }

我们可以通过unique_lockowns_lock判断是否持有锁

  1. //可判断是否占有锁
  2. void owns_lock() {
  3. //lock可自动解锁,也可手动解锁
  4. std::unique_lock<std::mutex> lock(mtx);
  5. shared_data++;
  6. if (lock.owns_lock()) {
  7. std::cout << "owns lock" << std::endl;
  8. }
  9. else {
  10. std::cout << "doesn't own lock" << std::endl;
  11. }
  12. lock.unlock();
  13. if (lock.owns_lock()) {
  14. std::cout << "owns lock" << std::endl;
  15. }
  16. else {
  17. std::cout << "doesn't own lock" << std::endl;
  18. }
  19. }

上述代码输出

  1. owns lock
  2. doesn't own lock

unique_lock可以延迟加锁

  1. //可以延迟加锁
  2. void defer_lock() {
  3. //延迟加锁
  4. std::unique_lock<std::mutex> lock(mtx, std::defer_lock);
  5. //可以加锁
  6. lock.lock();
  7. //可以自动析构解锁,也可以手动解锁
  8. lock.unlock();
  9. }

那我们写一段代码综合运用owns_lockdefer_lock

  1. //同时使用owns和defer
  2. void use_own_defer() {
  3. std::unique_lock<std::mutex> lock(mtx);
  4. // 判断是否拥有锁
  5. if (lock.owns_lock())
  6. {
  7. std::cout << "Main thread has the lock." << std::endl;
  8. }
  9. else
  10. {
  11. std::cout << "Main thread does not have the lock." << std::endl;
  12. }
  13. std::thread t([]() {
  14. std::unique_lock<std::mutex> lock(mtx, std::defer_lock);
  15. // 判断是否拥有锁
  16. if (lock.owns_lock())
  17. {
  18. std::cout << "Thread has the lock." << std::endl;
  19. }
  20. else
  21. {
  22. std::cout << "Thread does not have the lock." << std::endl;
  23. }
  24. // 加锁
  25. lock.lock();
  26. // 判断是否拥有锁
  27. if (lock.owns_lock())
  28. {
  29. std::cout << "Thread has the lock." << std::endl;
  30. }
  31. else
  32. {
  33. std::cout << "Thread does not have the lock." << std::endl;
  34. }
  35. // 解锁
  36. lock.unlock();
  37. });
  38. t.join();
  39. }

上述代码回依次输出, 但是程序会阻塞,因为子线程会卡在加锁的逻辑上,因为主线程未释放锁,而主线程又等待子线程退出,导致整个程序卡住。

  1. Main thread has the lock.
  2. Thread does not have the lock.

lock_guard一样,unique_lock也支持领养锁

  1. //同样支持领养操作
  2. void use_own_adopt() {
  3. mtx.lock();
  4. std::unique_lock<std::mutex> lock(mtx, std::adopt_lock);
  5. if (lock.owns_lock()) {
  6. std::cout << "owns lock" << std::endl;
  7. }
  8. else {
  9. std::cout << "does not have the lock" << std::endl;
  10. }
  11. lock.unlock();
  12. }

尽管是领养的,但是打印还是会出现owns lock,因为不管如何锁被加上,就会输出owns lock

既然unique_lock支持领养操作也支持延迟加锁,那么可以用两种方式实现前文lock_guard实现的swap操作。

  1. //之前的交换代码可以可以用如下方式等价实现
  2. int a = 10;
  3. int b = 99;
  4. std::mutex mtx1;
  5. std::mutex mtx2;
  6. void safe_swap() {
  7. std::lock(mtx1, mtx2);
  8. std::unique_lock<std::mutex> lock1(mtx1, std::adopt_lock);
  9. std::unique_lock<std::mutex> lock2(mtx2, std::adopt_lock);
  10. std::swap(a, b);
  11. //错误用法
  12. //mtx1.unlock();
  13. //mtx2.unlock();
  14. }
  15. void safe_swap2() {
  16. std::unique_lock<std::mutex> lock1(mtx1, std::defer_lock);
  17. std::unique_lock<std::mutex> lock2(mtx2, std::defer_lock);
  18. //需用lock1,lock2加锁
  19. std::lock(lock1, lock2);
  20. //错误用法
  21. //std::lock(mtx1, mtx2);
  22. std::swap(a, b);
  23. }

大家注意一旦mutexunique_lock管理,加锁和释放的操作就交给unique_lock,不能调用mutex加锁和解锁,因为锁的使用权已经交给unique_lock了。

我们知道mutex是不支持移动和拷贝的,但是unique_lock支持移动,当一个mutex被转移给unique_lock后,可以通过unique_ptr转移其归属权.

  1. //转移互斥量所有权
  2. //互斥量本身不支持move操作,但是unique_lock支持
  3. std::unique_lock <std::mutex> get_lock() {
  4. std::unique_lock<std::mutex> lock(mtx);
  5. shared_data++;
  6. return lock;
  7. }
  8. void use_return() {
  9. std::unique_lock<std::mutex> lock(get_lock());
  10. shared_data++;
  11. }

锁的粒度表示加锁的精细程度,一个锁的粒度要足够大,保证可以锁住要访问的共享数据。

同时一个锁的粒度要足够小,保证非共享数据不被锁住影响性能。

unique_ptr则很好的支持手动解锁。

  1. void precision_lock() {
  2. std::unique_lock<std::mutex> lock(mtx);
  3. shared_data++;
  4. lock.unlock();
  5. //不设计共享数据的耗时操作不要放在锁内执行
  6. std::this_thread::sleep_for(std::chrono::seconds(1));
  7. lock.lock();
  8. shared_data++;
  9. }

共享锁

试想这样一个场景,对于一个DNS服务,我们可以根据域名查询服务对应的ip地址,它很久才更新一次,比如新增记录,删除记录或者更新记录等。平时大部分时间都是提供给外部查询,对于查询操作,即使多个线程并发查询不加锁也不会有问题,但是当有线程修改DNS服务的ip记录或者增减记录时,其他线程不能查询,需等待修改完再查询。或者等待查询完,线程才能修改。也就是说读操作并不是互斥的,同一时间可以有多个线程同时读,但是写和读是互斥的,写与写是互斥的,简而言之,写操作需要独占锁。而读操作需要共享锁。

要想使用共享锁,需使用共享互斥量std::shared_mutex,std::shared_mutex是C++17标准提出的。
C++14标准可以使用std::shared_time_mutex,

std::shared_mutexstd::shared_timed_mutex 都是用于实现多线程并发访问共享数据的互斥锁,但它们之间存在一些区别:

  1. std::shared_mutex
  1. * 提供了 `lock()`, `try_lock()`, `try_lock_for()` 以及 `try_lock_until()` 函数,这些函数都可以用于获取互斥锁。
  2. * 提供了 `try_lock_shared()` `lock_shared()` 函数,这些函数可以用于获取共享锁。
  3. * `std::shared_mutex` 被锁定后,其他尝试获取该锁的线程将会被阻塞,直到该锁被解锁。
  1. std::shared_timed_mutex
  1. * `std::shared_mutex` 类似,也提供了 `lock()`, `try_lock()`, `try_lock_for()` 以及 `try_lock_until()` 函数用于获取互斥锁。
  2. * `std::shared_mutex` 不同的是,它还提供了 `try_lock_shared()` `lock_shared()` 函数用于获取共享锁,这些函数在尝试获取共享锁时具有超时机制。
  3. * `std::shared_timed_mutex` 被锁定后,其他尝试获取该锁的线程将会被阻塞,直到该锁被解锁,这与 `std::shared_mutex` 相同。然而,当尝试获取共享锁时,如果不能立即获得锁,`std::shared_timed_mutex` 会设置一个超时,超时过后如果仍然没有获取到锁,则操作将返回失败。

因此,std::shared_timed_mutex 提供了额外的超时机制,这使得它在某些情况下更适合于需要处理超时的并发控制。然而,如果不需要超时机制,可以使用更简单的 std::shared_mutex

C++11标准没有共享互斥量,可以使用boost提供的boost::shared_mutex

如果我们想构造共享锁,可以使用std::shared_lock,如果我们想构造独占锁, 可以使用std::lock_gurad.

我们用一个类DNService代表DNS服务,查询操作使用共享锁,而写操作使用独占锁,可以是如下方式的。

  1. class DNService {
  2. public:
  3. DNService() {}
  4. //读操作采用共享锁
  5. std::string QueryDNS(std::string dnsname) {
  6. std::shared_lock<std::shared_mutex> shared_locks(_shared_mtx);
  7. auto iter = _dns_info.find(dnsname);
  8. if (iter != _dns_info.end()) {
  9. return iter->second;
  10. }
  11. return "";
  12. }
  13. //写操作采用独占锁
  14. void AddDNSInfo(std::string dnsname, std::string dnsentry) {
  15. std::lock_guard<std::shared_mutex> guard_locks(_shared_mtx);
  16. _dns_info.insert(std::make_pair(dnsname, dnsentry));
  17. }
  18. private:
  19. std::map<std::string, std::string> _dns_info;
  20. mutable std::shared_mutex _shared_mtx;
  21. };

QueryDNS 用来查询dns信息,多个线程可同时访问。
AddDNSInfo 用来添加dns信息,属独占锁,同一时刻只有一个线程在修改。

递归锁

有时候我们在实现接口的时候内部加锁,接口内部调用完结束自动解锁。会出现一个接口调用另一个接口的情况,如果用普通的std::mutex就会出现卡死,因为嵌套加锁导致卡死。但是我们可以使用递归锁。

但我个人并不推荐递归锁,可以从设计源头规避嵌套加锁的情况,我们可以将接口相同的功能抽象出来,统一加锁。下面的设计演示了如何使用递归锁

  1. class RecursiveDemo {
  2. public:
  3. RecursiveDemo() {}
  4. bool QueryStudent(std::string name) {
  5. std::lock_guard<std::recursive_mutex> recursive_lock(_recursive_mtx);
  6. auto iter_find = _students_info.find(name);
  7. if (iter_find == _students_info.end()) {
  8. return false;
  9. }
  10. return true;
  11. }
  12. void AddScore(std::string name, int score) {
  13. std::lock_guard<std::recursive_mutex> recursive_lock(_recursive_mtx);
  14. if (!QueryStudent(name)) {
  15. _students_info.insert(std::make_pair(name, score));
  16. return;
  17. }
  18. _students_info[name] = _students_info[name] + score;
  19. }
  20. //不推荐采用递归锁,使用递归锁说明设计思路并不理想,需优化设计
  21. //推荐拆分逻辑,将共有逻辑拆分为统一接口
  22. void AddScoreAtomic(std::string name, int score) {
  23. std::lock_guard<std::recursive_mutex> recursive_lock(_recursive_mtx);
  24. auto iter_find = _students_info.find(name);
  25. if (iter_find == _students_info.end()) {
  26. _students_info.insert(std::make_pair(name, score));
  27. return;
  28. }
  29. _students_info[name] = _students_info[name] + score;
  30. return;
  31. }
  32. private:
  33. std::map<std::string, int> _students_info;
  34. std::recursive_mutex _recursive_mtx;
  35. };

我们可以看到AddScore函数内部调用了QueryStudent, 所以采用了递归锁。

但是我们同样可以改变设计,将两者公有的部分抽离出来生成一个新的接口AddScoreAtomic.

AddScoreAtomic可以不适用递归锁,照样能完成线程安全操作的目的。

总结

本文介绍了unique_lock,共享锁,递归锁等的使用,较为全面的介绍了这几种锁的使用场景和潜在风险。

视频链接

https://space.bilibili.com/271469206/channel/collectiondetail?sid=1623290

源码链接

https://gitee.com/secondtonone1/boostasio-learn

热门评论

热门文章

  1. 解密定时器的实现细节

    喜欢(566) 浏览(3060)
  2. C++ 类的继承封装和多态

    喜欢(588) 浏览(4329)
  3. Linux环境搭建和编码

    喜欢(594) 浏览(10073)
  4. windows环境搭建和vscode配置

    喜欢(587) 浏览(2335)
  5. slice介绍和使用

    喜欢(521) 浏览(2235)

最新评论

  1. 泛型算法的定制操作 secondtonone1:lambda和bind是C11新增的利器,善于利用这两个机制可以极大地提升编程安全性和效率。
  2. 类和对象 陈宇航:支持!!!!
  3. C++ 虚函数表原理和类成员内存分布 WangQi888888:class Test{ int m; int b; }中b成员是int,为什么在内存中只占了1个字节。不应该是4个字节吗?是不是int应该改为char。这样的话就会符合图上说明的情况
  4. 解决博客回复区被脚本注入的问题 secondtonone1:走到现在我忽然明白一个道理,无论工作也好生活也罢,最重要的是开心,即使一份安稳的工作不能给我带来事业上的积累也要合理的舍弃,所以我还是想去做喜欢的方向。
  5. asio多线程模型IOServicePool Lion:线程池一定要继承单例模式吗

个人公众号

个人微信