《C++ Concurrency in Action》笔记10 选择合适的lock粒度

先看一个简单的示例:

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的操作间隙内以及比较两个值的拷贝之前,其他线程可能更改任何一个对象的值,甚至是交换了两个对象。因此得到的结果是不准确的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值