先看一个简单的示例:
class X
{
public:
string some_detail;
std::mutex m;
public:
X(string 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);//先不锁定
std::unique_lock<std::mutex> lock_b(rhs.m, std::defer_lock);
std::lock(lock_a, lock_b);//锁定
swap(lhs.some_detail, rhs.some_detail);
}
};
在swap函数中,先使用unique_lock接收两个对象的mutex,注意第二个参数是defer_lock对象,代表构造时不锁定,稍后再锁定。然后使用了lock函数将两个mutex同时锁定。
在swap函数退出后,两个unique_lock对象的析构函数分别负责解锁其接收的mutex。
从这里看出,unique_lock比lock_guard更为灵活。但是unique_lock因为要存储一个bool型的状态量,因此其内存使用相对大一点,另外由于要经常检查和更新这个bool量所以其性能也会比lock_guard稍微慢一点点。所以一般情况下,建议使用lock_guard。这些情况下需要使用unique_lock:1.先绑定mutex稍后再锁定。2.需要将lock的所有权转交给其他范围。
因为unique_lock并不强迫去拥有mutex的所有权,因此可以通过std::move()函数来将mutex的所有权在uqnique_lokc的实例之间转移。有时,这种转换是自动的,有时则需要显示使用std::move,这取决于被转移的对象是左值还是右值。如果是右值则自动转换,如果是左值则必须使用std::move函数。lokc_guard因为没有记录状态用的bool型,因此没有提供移动构造和移动赋值函数,也就不支持move了。
关于左值,右值在这里多说几句。匿名对象是右值。所有的变量,除了在函数中作为返回值的局部变量外,不管是左值变量还是右值变量都是左值:
例如:
void f(string &&str)
{
string s = std::move(str);
...
}
即使函数f的参数str类型是右值引用,但是str本身仍然是个左值,因此必须使用std::move函数来转换成右值。
unique_lock类,是可以move的,但不是可copy的。
一种情况是,允许一个函数锁定一个mutex,然后将其控制权移交给调用者,以便调用者可以在同一个mutex上继续执行其他操作。下面是一段示例代码:
std::unique_lock<std::mutex> get_lock()
{
extern std::mutex some_mutex;
std::unique_lock<std::mutex> lk(some_mutex);
prepare_data();
return lk;
}
void process_data()
{
std::unique_lock<std::mutex> lk(get_lock());
do_something();
}
由于some_mutex是个局部变量,因此可以作为右值直接当作返回值。返回后,process_data()函数接收了mutex的控制权,延续了这个处理过程,此过程执行期间外部线程无法锁定some_mutex,保证了处理过程的连续性。
有一种典型的用法,通过一个包装类,封装mutex,通过一个函数获取该类的一个实例,获取后实例后mutex即处于锁定状态,然后调用成员函数来处理数据,如果想要其他函数继续处理这个对象,需要move该对象。对象销毁时解锁。
unique_lock还允许在对象销毁前调用unlock()函数。当锁已经不再必要时可以主动解锁。这就意味着可以尽量减少锁定时间,避免不必要的长期锁定一个mutex,从而提高性能。如果不是必须,那么尽可能不使用锁,如果必须使用锁,那么尽可能缩短锁定的时间,以便于其他线程访问共享数据。
当一个线程独占一个锁的时候,不要做耗时的操作,比如文件I/O操作。文件I/O操作的的耗时程度至少是直接从内存中读写数据的上百倍。所以,如果不是必须,则不要在锁定后进行文件I/O操作。还有就是,尽量避免独占一个锁后再次请求其他锁。
unique_lock处理这种事情非常适合,你可以在不需要锁定时执行unlock(),过后如果再次需要锁定则再一次执行lock()函数。就像下面这样:
void get_and_process_data()
{
std::unique_lock<std::mutex> my_lock(the_mutex);
some_class data_to_process=get_next_data_chunk();
my_lock.unlock();
result_type result=process(data_to_process);
my_lock.lock();
write_result(data_to_process,result);
}
向之前讲述的那样,为了swap两个比较大的对象时,需要同时锁定两个对象。现在考虑另一种情况,如果我们现在需要compare两个对象,而且这两个对象的成员仅仅是int。拷贝int指挥花费很少的时间,几乎可以不计,那么你可能会这样设计compare:先锁定一个对象,然后拷贝数据成员,然后解锁,接着对另一个对象做相同操作。最好将两个数据拷贝作比较。就像下面这样,但这样的实现也并不可取:
class Y
{
private:
int some_detail;
mutable std::mutex m;
int get_detail() const
{
std::lock_guard<std::mutex> lock_a(m);
return some_detail;
}
public:
Y(int sd) :some_detail(sd) {}
friend bool operator==(Y const& lhs, Y const& rhs)
{
if (&lhs == &rhs)
return true;
int const lhs_value = lhs.get_detail();
int const rhs_value = rhs.get_detail();
return lhs_value == rhs_value;
}
};
表面上看,既能保证锁定时间很短,又避免了同时锁定2个mutex。但是在两次锁定mutex的操作间隙内以及比较两个值的拷贝之前,其他线程可能更改任何一个对象的值,甚至是交换了两个对象。因此得到的结果是不准确的。