C++多线程编程中通常会对共享的数据进行写保护,以防止多线程在对共享数据成员进行读写时造成资源争抢导致程序出现未定义的行为。通常的做法是在使用修改共享数据成员的时候进行加锁--mutex。在使用锁的时候通常是在对共享数据进行修改之前进行lock操作,在写完之后再进行unlock操作,但过多的使用锁,有时会出现由于疏忽导致由于lock之后在离开共享成员操作区域时忘记unlock,导致死锁。
针对以上的问题,C++11中引入了std::unique_lock与std::lock_guard两种数据结构。通过对lock和unlock进行一次薄的封装,实现自动unlock的功能。
相对于std::lock_guard来说,std::unique_lock更加灵活,std::unique_lock不拥有与其关联的mutex。构造函数的第二个参数可以指定为std::defer_lock,这样表示在构造unique_lock时,传入的mutex保持unlock状态。然后通过调用std::unique_lock对象的lock()方法或者将将std::unique_lock对象传入std::lock()方法来锁定mutex。
#include
class some_big_object
{
...
};
void swap(some_big_object& lhs,some_big_object& rhs)
{
...
}
class X
{
private:
some_big_object some_detail;
mutable std::mutex m;
public:
X(some_big_object const& sd):some_detail(sd){}
friend void swap(X& lhs, X& rhs)
{
if(&lhs==&rhs)
return;
// 构造unique_lock,保持mutex为unlocked状态
std::unique_lock lock_a(lhs.m,std::defer_lock);
std::unique_lock lock_b(rhs.m,std::defer_lock);
// lock mutex
std::lock(lock_a,lock_b);
swap(lhs.some_detail,rhs.some_detail);
}
};
int main()
{
}
std::unique_lock比std::lock_guard需要更大的空间,因为它需要存储它所关联的mutex是否被锁定,如果被锁定,在析构该std::unique_lock时,就需要unlock它所关联的mutex。std::unique_lock的性能也比std::lock_guard稍差,因为在lock或unlock mutex时,还需要更新mutex是否锁定的标志。大多数情况下,推荐使用std::lock_guard但是如果需要更多的灵活性,比如上面这个例子,或者需要在代码之间传递lock的所有权,这可以使用std::unique_lock。
std::unique_lock并不拥有与其关联的mutex,mutex的所有权可以在不同的实例之间进行传递。比如我们期望提供一个函数,在锁定mutex后,将mutex的所有权返回给调用者,这样可以让调用者在mutex的保护下,执行更多额外的操作。如下所示的get_lock()函数,锁定mutex后,执行prepare_data(),然后将mutex返回给调用者,调用者将mutex传入其自己的局部变量std::unique_lock,然后mutex的保护下调用do_something()。当process_data()退出时,会自动unlock这个mutex。
std::unique_lock get_lock()
{
extern std::mutex some_mutex;
std::unique_lock lk(some_mutex);
prepare_data();
return lk;
}
void process_data()
{
std::unique_lock lk(get_lock());
do_something();
}
std::unique_lock的灵活性还在于我们可以主动的调用unlock()方法来释放mutex,因为锁的时间越长,越会影响程序的性能,在一些特殊情况下,提前释放mutex可以提高程序执行的效率。
此外,我们还需要注意锁的粒度,如果有多个线程等待相同的资源,而某个线程长时间的持有mutex,就会增加其它线程的等待时间。我们要尽量保证只对共享数据加锁,在锁定范围之外对数据进行处理。不要在锁定mutex的情况下执行I/O操作,因为I/O操作是很慢的。可以在必要的情况下调用std::unique_lock的unlock()操作来释放mutex,在需要时,再调用lock()来锁定mutex。
void get_and_process_data()
{
std::unique_lock my_lock(the_mutex);
some_class data_to_process=get_next_data_chunk();
my_lock.unlock(); // 在process中不需要锁定mutex
result_type result=process(data_to_process);
my_lock.lock(); // 在写操作前再次锁定mutex
write_result(data_to_process,result);
}
在swap实例中,我们锁定了两个对象的mutex再进行比较交换操作。假设我们要比较两个对象,而复制对象的代价很小,则可以考虑减小mutex保护的范围和时间,在锁定mutex时,对对象进行复制,释放mutex后,用两个复制的对象来进行比较操作。
#include
class Y
{
private:
int some_detail;
mutable std::mutex m;
// 返回对象的拷贝
int get_detail() const
{
// 保护对象
std::lock_guard 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;
}
};
int main()
{
...
}
采用这种方法,虽然减小了mutex锁定的范围和时间,但是却改变了比较的语义。因为这种实现方法只能保证某一时刻读取的lhs.some_detail和另一时刻读取的rhs.some_detail相等,但是在读取lhs_value和读取lrh_value之间存在race condition,它们的值可能已经被修改了。