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

来源:互联网 发布:网络流行词2017 编辑:程序博客网 时间:2024/06/05 19:28

先看一个简单的示例:

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
原创粉丝点击