需要注意下面方法逐渐增加灵活性,但是增加了时间和空间。
方法一:
1.lock
使用lock函数对互斥元锁定。如果当前锁已经上锁,则会阻塞。
2.try_lock
使用try_lock对互斥元进行上锁,区别于lock,如果当前已上锁,则不会阻塞。
3.try_lock_for和try_lock_until
使用方法如字面意思
4.unlock
对锁进行解锁
缺点:在异常处理时候,没办法自动解锁,会导致死锁
接下来方法都是对lock和unlock的封装
方法二:
std::lock_guard lck(mtx);
构造既锁定,析构会对互斥元进行解锁。
也就是说在子线程函数中执行,使用lock_guard会在函数结束时候自动析构,从而解锁
存在一个问题:若在lock_guard析构调用前,互斥元提前结束,则在析构函数中会出现空指针异常。
C++17可忽略模板参数列表 std::lock_guard lck(mtx);
注意:虽然说上述上锁与解锁十分方便,但是对于数据并不能得到有效保护,因为在使用函数时,可通过指针或引用,来获得保护数据。
因此:不要将对受保护对象的指针或引用传递到锁范围之外。
问题:对于上面两种方式,方法二虽然解决了异常下,锁的自动释放,但是无法解决,线程之间自己产生的死锁,因为无法直接解锁,所以很容易产生死锁
于是产生了方法三。
方法三:
使用unique_lock,存在两个参数//提醒一下,lock_guard也存在两个参数
unique_lock (mutex_type& m, try_to_lock_t tag);
第二个参数:
默认:加锁,需要之前没有clock(),阻塞
defer_lock:初始化一个没有clock的对象,不能提前clock();
try_to_lock:之前没有clock,不会阻塞
adopt_clock:需要提前clock。构造函数也就不需要clock了。
对于unique_lock创建的实例,可使用clock和unlock进行对锁的灵活控制,并且其析构函数自动帮你释放锁。
存在以下成员函数:
own_lock返回是否拥有锁
release释放与锁的相关联,返回mutex指针,加锁的mutex在release,后要利用指针unclock
该函数优点是,可以将所有权转移,也就是不一定要在一个unique_lock吊死。
可通过unique_lock的复制函数转移所有权,也可以通过函数return返回unique_lock对象。
其他工具:
初始化问题:因为某些资源比如文件进行读写需要查看是否初始化,而初始化创建开销不小。因此延迟初始化。
对于这类资源的操作,需要对每个线程查看是否初始化,才能使用。
void func()
{
std:unique_clock<std::mutex> clk(mtx);
if(!资源初始化)
{
//存在问题:在此期间,线程1阻塞,其他线程判断没有初始化,进行初始化
资源初始化
}
do_somthing();
}
双重检验锁定模式:
void func()
{
if(!资源初始化)//无锁条件下读取
{
//问题:出现条件竞争
std:unique_clock<std::mutex> clk(mtx);
if(!资源初始化)//在判断和上锁期间,其他线程可能完成初始化,所以再次判断
资源初始化
}
do_somthing();
}
解决方式:使用std::onec_flag类和std::call_once函数
template <class Fn, class... Args>
void call_once (once_flag& flag, Fn&& fn, Args&&... args);
std::once_flag resource_flag;
void init_resource()
{
资源初始化
}
void func()
{
std::call_once(resource_flag,init_resource);
do_somthing();
}
另外
C++17所增加的shared_mutex,shared_lock关注点在于读写锁
scoped_lock预防死锁方式为破坏请求并保持条件。