概述
三、mutex
1、基本mutex
定义在头文件mutex中,std命名空间。
#include <mutex>
基本mutex的用法非常普通,和pthread中无异,在c语言的编程中,对于mutex的lock和unlock是较为麻烦的,因为要关注正常退出和异常退出时,锁的释放,否则将容易造成死锁,还有线程被cancel时候锁的释放。
在c++中聪明的利用了对象销毁时自动析构的机制,采用了RAII思想,对锁进行了封装管理,所以一般在使用c++的锁时,应该是不建议直接对mutex对象进行操作的,而应该借助这些管理类型来操作。
2.基本的两个锁管理类:lock_guard, scoped_lock
- lock_guard
lockguard类型只有构造函数和析构函数,只提供最简单的RAII机制,既初始化时获取mutex并锁定,出定义域时析构函数里自动释放锁,这就解决了之前提出的那些麻烦的场景问题,因为不管时cancel了线程还是出现异常退出了线程,变量都将退出定义域,析构函数都将自动调用,锁都会释放。
该类型只能用于锁定一个锁,一般用于某个函数只对一个mutex管理下的数据进行修改访问时使用:
//省略各种头文件
std::mutex one_mutex;
int data=0;
void modify_shared_data(){
std::lock_guard<std::mutex> guard(one_mutex);
data++;
std::cout<<"Now, data is: "<<data<<std::endl;
} //guard的析构函数自动释放锁
int main()
{
std::vector<std::thread> threads;
for(int i=0;i<100;++i)
threads.emplace_back(modify_shared_data);
for(int i=0;i<100;++i)
threads[i].join();
return 0;
}
有时,需要在函数中间的地方短暂的释放锁,然后再重新获取它,看似用lock_guard类型貌似无法实现,其实是可以的,手册的例程给了很好的展示:
//省略各种头文件
std::mutex one_mutex;
int data=0;
void modify_shared_data(int i){
using namespace std::chrono_literals;
{
std::lock_guard<std::mutex> guard(one_mutex);
data++;
std::cout<<"This is thread: "<<i<<", Now data is: "<<data<<std::endl;
}//使用大括号分离定义域来达到目的
std::this_thread::sleep_for(2ms);
{
std::lock_guard<std::mutex> guard(one_mutex);
data++;
std::cout<<"This is thread: "<<i<<", Now data is: "<<data<<std::endl;
}
}
int main()
{
std::vector<std::thread> threads;
for(int i=0;i<100;++i)
threads.emplace_back(modify_shared_data);
for(int i=0;i<100;++i)
threads[i].join();
return 0;
}
- 接下来谈scope_lock,使用该类型需要在g++编译时带参数-std=c++17,明确激活c++17的特性
scope_lock和lock_guard所有特点基本相同,都只有构造函数和析构函数,不能复制不能移动(不能移动这个我感觉的,没测试过),唯一不同的地方是,可以在构造时传入多个锁,并一起锁定,由该类型的构造函数保证此处不死锁。
题外话:如何避免死锁
谈到死锁了,就想总结一下自己的理解,一个锁的时候只要不自己锁自己一般都不会死锁,但多个锁要一起锁定的时候,很容易发生死锁,原因在于多个线程如果发生竞争,会发生每个线程都持有一把锁,然后又要获取其他人的锁,变成A跟B说你把锁给我,B说不行,你先把锁给我的场景,最后就是谁也不能同时获得两个锁,互相僵持。同时锁定的锁越多,这种竞争概率就越大。
但这种锁竞争是可以有策略避免的,比如上面A和B两个锁,如果所有线程都约定如果要两个锁,那必须先抢A锁再抢B锁,这样多个线程永远都不会死锁。这个思路的关键就在于约定一个统一的顺序,这样大家不会产生每个人持有部分锁的情况,死锁就可以解决。
还有一种锁叫层次锁也是一个原理,将锁的获取顺序设计成单向线性的协议,就可以避免这种死锁。当然实践中还会有一些其他类型的死锁,需要根据具体问题再分析。我猜这个scope_lock以及lock函数,来避免死锁的算法可能也是类似的,比如给每个mutex对象附带一个数字id,然后锁的时候按id从小到大以此锁定,嗯,瞎猜的。
lock_guard和scope_lock这两个类型我将其归为一类,下一篇总结unique_lock和shared_lock两个更为灵活的锁管理类,可以随意lock和unlock,感觉上就是给mutex穿了件衣服,使它出了定义域后能够自动释放锁。
但要注意lock_guard, scope_lock和unique_lock, shared_lock不是同级的关系,虽然功能上他们都是提供了析构函数自动释放锁的功能,但不是同级。
真要说起来:
- 【mutex】最原始为一级,
- 【unique_lock, shared_lock】为第二级,给mutex穿了一个衣服,
- 【lock_guard, scope_lock】为第三级,用来自动管理【mutex】或者【unique_lock, shared_lock】,它的感觉就是只要这个东西能lock和unlock,它都可以管理
lock_guard<std::mutex> ...
//or
lock_guard<std::unique_lock<std::mutex>> ...
//两种都可以
最后
以上就是傲娇康乃馨为你收集整理的c++多线程库手册学习笔记(二)的全部内容,希望文章能够帮你解决c++多线程库手册学习笔记(二)所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复