std::unqiue_lock
使用更为自由的不变量,这样std::unique_lock
实例不会总与互斥量的数据类型相关,使用起来要比std:lock_guard
更加灵活。首先,可将std::adopt_lock
作为第二个参数传入构造函数,对互斥量进行管理;也可以将std::defer_lock
作为第二个参数传递进去,表明互斥量应保持解锁状态。这样,就可以被std::unique_lock
对象(不是互斥量)的lock()函数的所获取,或传递std::unique_lock
对象到std::lock()
中。使用std::unique_lock
和std::defer_lock
①,而非std::lock_guard
和std::adopt_lock
。代码长度相同,几乎等价,唯一不同的就是:std::unique_lock
会占用比较多的空间,并且比std::lock_guard
稍慢一些。保证灵活性要付出代价,这个代价就是允许std::unique_lock
实例不带互斥量:信息已被存储,且已被更新。下面举一个例子:
class some_big_object;
void swap(some_big_object& lhs,some_big_object& rhs);
class X
{
private:
some_big_object some_detail;
std::mutex m;
public:
X(some_big_object const& sd):some_detail(sd){}
friend void swap(X& lhs, X& rhs)
{
if(&lhs==&rhs)
return;
std::unique_lock<std::mutex> lock_a(lhs.m,std::defer_lock); // 1
std::unique_lock<std::mutex> lock_b(rhs.m,std::defer_lock); // 1 std::def_lock 留下未上锁的互斥量
std::lock(lock_a,lock_b); // 2 互斥量在这里上锁
swap(lhs.some_detail,rhs.some_detail);
}
};
因为std::unique_lock
支持lock(), try_lock()和unlock()成员函数,所以能将std::unique_lock
对象传递到std::lock()
②。这些同名的成员函数在低层做着实际的工作,并且仅更新std::unique_lock
实例中的标志,来确定该实例是否拥有特定的互斥量,这个标志是为了确保unlock()在析构函数中被正确调用。如果实例拥有互斥量,那么析构函数必须调用unlock();但当实例中没有互斥量时,析构函数就不能去调用unlock()。这个标志可以通过owns_lock()成员变量进行查询。
可能如你期望的那样,这个标志被存储在某个地方。因此,std::unique_lock
对象的体积通常要比std::lock_guard
对象大,当使用std::unique_lock
替代std::lock_guard
,因为会对标志进行适当的更新或检查,就会做些轻微的性能惩罚。当std::lock_guard
已经能够满足你的需求,那么还是建议你继续使用它。当需要更加灵活的锁时,最好选择std::unique_lock
,因为它更适合于你的任务。你已经看到一个递延锁的例子,另外一种情况是锁的所有权需要从一个域转到另一个域。
不同域中互斥量所有权的传递
std::unique_lock
实例没有与自身相关的互斥量,一个互斥量的所有权可以通过移动操作,在不同的实例中进行传递。某些情况下,这种转移是自动发生的,例如:当函数返回一个实例;另些情况下,需要显式的调用std::move()
来执行移动操作。从本质上来说,需要依赖于源值是否是左值——一个实际的值或是引用——或一个右值——一个临时类型。当源值是一个右值,为了避免转移所有权过程出错,就必须显式移动成左值。std::unique_lock
是可移动,但不可赋值的类型。附录A,A.1.1节有更多与移动语句相关的信息。
一种使用可能是允许一个函数去锁住一个互斥量,并且将所有权移到调用者上,所以调用者可以在这个锁保护的范围内执行额外的动作。
下面的程序片段展示了:函数get_lock()锁住了互斥量,然后准备数据,返回锁的调用函数:
std::unique_lock<std::mutex> get_lock()
{
extern std::mutex some_mutex;
std::unique_lock<std::mutex> lk(some_mutex);
prepare_data();
return lk; // 1
}
void process_data()
{
std::unique_lock<std::mutex> lk(get_lock()); // 2
do_something();
}
lk在函数中被声明为自动变量,它不需要调用std::move()
,可以直接返回①(编译器负责调用移动构造函数)。process_data()函数直接转移std::unique_lock
实例的所有权②,调用do_something()可使用的正确数据(数据没有受到其他线程的修改)。
通常这种模式会用于已锁的互斥量,其依赖于当前程序的状态,或依赖于传入返回类型为std::unique_lock
的函数(或以参数返回)。这样的用法不会直接返回锁,不过网关类的一个数据成员可用来确认已经对保护数据的访问权限进行上锁。这种情况下,所有的访问都必须通过网关类:当你想要访问数据,需要获取网关类的实例(如同前面的例子,通过调用get_lock()之类函数)来获取锁。之后你就可以通过网关类的成员函数对数据进行访问。当完成访问,可以销毁这个网关类对象,将锁进行释放,让别的线程来访问保护数据。这样的一个网关类可能是可移动的(所以他可以从一个函数进行返回),在这种情况下锁对象的数据必须是可移动的。
std::unique_lock
的灵活性同样也允许实例在销毁之前放弃其拥有的锁。可以使用unlock()来做这件事,如同一个互斥量:std::unique_lock
的成员函数提供类似于锁定和解锁互斥量的功能。std::unique_lock
实例在销毁前释放锁的能力,当锁没有必要在持有的时候,可以在特定的代码分支对其进行选择性的释放。这对于应用性能来说很重要,因为持有锁的时间增加会导致性能下降,其他线程会等待这个锁的释放,避免超越操作。