上文介绍了使用std::lock()
同时锁定两个互斥元来避免死锁。《并发编程》对如何避免死锁进行了总结:
- 避免嵌套所
- 在持有锁是,避免调用用户提供的代码
- 以固定顺序获取锁
- 使用层次锁
这里不再对这些方法进行探讨。
使用std:unique_lock
灵活锁定
先看一个std::unique
的例子:
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);
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);
}
};
std::unique_lock
相比std::lock_guard
提供了更多的灵活性。示例代码中构造std::unique_lock
的第二个参数std::defer_lock
,表示将第一个参数给定的互斥元保留为未被锁定。这样可以在之后需要的时候调用std::unique_lock
的成员函数lock()
或者将其传入std::lock()
来锁定。而对于std::lock_guard
,其进行构造时,互斥元已经被锁定。std::unique_lock
实例内部需要一个标识来表示它是否拥有互斥元,这样才能保证它在析构时调用unlock()
释放锁。标准库提供了owns_lock()
成员函数查询是否拥有互斥元。显然,std::unique_lock
的实例会大于std::lock_guard
,且有些许性能损失。std::unique_lock
的这种灵活性同样使用于解锁,可以在其被销毁前调用unlock()
释放锁。
在作用域之间转移锁的所有权
std::unique_lock
具有移动语义,实例不拥有与其相关的互斥元,互斥元的所有权可以在实例之间移动,可以通过std::move()
显示完成。当其作为函数的返回值时会自动发生移动,参考下面的例子:
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();
}
在函数get_lock
返回时编译器会调用移动构造函数,该函数拥有的std::unique_lock
实例将会被移动到lk
。此例展示了。一个函数锁定了一个互斥元,再将锁的所有权转移给了它的调用者,后者可以在同一个锁的保护下进行其他操作。
锁定在恰当的粒度
锁的粒度是一个抽象描述,表示由某个锁保护的数据量。细粒度锁保护的数据量少,粗粒度锁保护的数据量大。选择一个合适的粒度至关重要。如果粒度太大,一个线程持有锁的时间太长,会增加其他线程等待的时间,因此尽量在实际访问共享数据的时候锁定互斥元,在锁的外面进行消耗时间的处理。这种情况下,std::unique_lock
是个不错的选择,因为可以在适当的适合进行lock()
和unlock()
。下面是一个例子:
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_tyoe result = process(data_to_process);
my_lock.lock();
write_result(data_to_process, result);
}
在处理数据时不需要上锁,因此提前进行unlock()
,再在处理完成之后写数据之前重新上锁。
再看另一个例子。本文的第一个例子是并发访问两个对象(X
类)的情况,如果是并发访问两个更简单的数据,比如int
类型,我们将粒度再次细化,得到如下代码:
class Y
{
private:
int some_detail;
mutable std::mutex m;
int get_detail() const
{
std::lock_guard<std::mutex> lock_a(m);
}
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;
}
};
比较运算符重载函数首先通过调用get_detail
获取要进行比较的值,该函数通过一个锁保护数据并其返回,重载函数在获取到数据之后再进行比较。锁的粒度细化了,且只有一个锁,这看起来是不是很美好?然而你仔细思考,在两次调用get_detail()
之间,lhs.detail
和rhs.detail
的值有可能发生变化,比如进行了一次交换操作,这样之后的比较还有意义吗?上面代码的真正语义是,lhs.detail
在一个时间点的值和rhs.detail
在另一个时间点的值相同,即使它们从未同时相等过。
0 条评论