c++并发编程实战(3)

在线程间共享数据

使用线程处理并发的一个好处是在线程间共享数据更加容易。

3.2 使用mutex保护数据

3.2.1在c++中使用mutex
  • 创建一个mutex std::mutex some_mutex;
  • 加锁 some_mutex.lock();
  • 解锁 some_mutex.unlock();

当一个线程试图锁定一个已被其它线程锁定的mutex时,它会被挂起直到mutex释放。

使用lock_guard

#include <list>
#include <mutex>
#include <algorithm>
std::list<int> some_list; 
std::mutex some_mutex; 
void add_to_list(int new_value)
{
    std::lock_guard<std::mutex> guard(some_mutex); 
    some_list.push_back(new_value);
}
bool list_contains(int value_to_find) 
{
    std::lock_guard<std::mutex> guard(some_mutex);  
    return std::find(some_list.begin(),some_list.end(),value_to_find)!= some_list.end();
}

add_to_list()list_contains()分别运行在不同的线程中。使用了一个全局共享的some_mutex来共享保护共享数据的访问。

3.2.4 死锁

当两个线程试图lock已被对方锁定的mutex时,就会发生死锁现象。所以当使用多个mutex时一个避免死锁的建议是大家都保护同一个次序对资源进行加锁。如果顺序规则依赖于参数,会有一些问题。比如写一个swap(lhs,rhs),如果两个线程按照相反的次序调用swap会出问题。这时我们可以使用std::lock()

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)      <--1
            return;
        std::lock(lhs.m,rhs.m); <--2 
        std::lock_guard<std::mutex> lock_a(lhs.m,std::adopt_lock); <--3 
        std::lock_guard<std::mutex> lock_b(rhs.m,std::adopt_lock); 
        swap(lhs.some_detail,rhs.some_detail);
    }
};

<--1 首先检测两个参数是否为同一对象。因为在mutex调用多次lock是一个末定义的行为。如果需要可重入的mutex,可使用std::recursive_mutex。

<--2 锁定多个对象。

<--3 adopt_lock参数告诉guard,mutex已加锁。

3.2.5 关于死锁的进一步指南

死锁不只出现在加锁时,比如两个线程调用join()互相等待对方退出时。死锁也不限于两个线程间,三个或三个以上纯种也会发生死锁。一个避免死锁的建议是“如果别的线程会等待你,那你就不要等待别的线程”。

一些规则

  • 只申请一个锁。

  第一个规则最简单,任何时候只申请一把锁。如果逻辑要求必须锁定多个资源时,使用std::lock()来保证次序。

  • 避免在持有锁的期间调用用户代码

如果调用了用户代码,你不知道这些代码做了什么,如果这些代码申请了锁资源,就没法保证遵守规则了。

  • 采用固定次序加锁
  • 使用级别锁
  • 扩展加锁指南
3.2.6 灵活使用std::unique_lock

std::unique_lockstd::lock_guard提供了更多的灵活性。unique_lock不必一直持有mutex。首先你可以在构造时使用std::adopt_lock做为第二个参数,来声明mutex已加锁。其次可以用std::defer_lock来告诉构造函数mutex会在以后加锁。延时锁定是通过调用unique_lock的lock()而不是mutex的lock的。注意unique_lock会比lock_guard大一点,同时性能差一些。

使用unique_lock来完成swap示例

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); <--1
        std::unique_lock<std::mutex> lock_b(rhs.m,std::defer_lock); 
        std::lock(lock_a,lock_b);   <--2
        swap(lhs.some_detail,rhs.some_detail);
    }
};

<--1此时mutex保持在unlock状态。<--2对mutex加锁。注意是用lock_a,和lock_b做为参数的,而不是用lhs.m和rhs.m可以使用unique_lock做为std::lock的参数是因为unique_lock支持lock(), try_lock(),和unlock()unique_lock内部持有一个标志来指明mutex是否上锁。并根据这个标志来决定在析构时是否调用unlock()。可以通过成员函数owns_lock()来得到这一标志。unique_lock通过损失一点性能来获得额外的灵活性,你可以根据具体情况来决定使用哪一个。这里的例子展示了延迟锁定的情况,另一种情况是将lock的所有权在不同做用域间传递。

3.2.7 传递mutex所有权

因为unique_lock不一定拥有相关联mutex的所有权,所以我们可以在不同实例间传递mutex的所有权。有时这一过程是自动的,如从函数返回一个实例或显示的调用std::move。

一个可能的使用情况是一个函数锁定一个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();
}
3.2.8 合适的锁定粒度

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_type result=process(data_to_process);
    my_lock.lock(); 
    write_result(data_to_process,result);
}

另一个示例

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; 
    }
};

只在get_detail中加锁,没有对整个operator==上锁。

3.3 其它保护共享数据工具

尽管mutex是最常用的方法,但在一些特殊情况下,有一些更高效的方法。如一个只读对象,只需要在初始化时进行保护。

3.3.1初始化时保护共享数据

假设有一个共享资源初始化的成本较高,通常我们用使用懒加载的方法。单线程时代码如下:

std::shared_ptr<some_resource> resource_ptr;
void foo()
{
    if(!resource_ptr)
    {
        resource_ptr.reset(new some_resource); 
    }
    resource_ptr->do_something();
}

多线程:

std::shared_ptr<some_resource> resource_ptr;
std::mutex resource_mutex;
void foo()
{
    std::unique_lock<std::mutex> lk(resource_mutex); 
    if(!resource_ptr)
    {
        resource_ptr.reset(new some_resource); 
    }
    lk.unlock();
    resource_ptr->do_something();
}

每次调用都进行加锁,效率低.

double checked 模式:

void undefined_behaviour_with_double_checked_locking()
{
    if(!resource_ptr)   <--1 
    {
        std::lock_guard<std::mutex> lk(resource_mutex);
        if(!resource_ptr)   <--2
        {
            resource_ptr.reset(new some_resource);  <--3 
        }
    }
    resource_ptr->do_something(); 
}

这一模式也有一些问题<--1<--3没有被锁保护。而且竞争不只存在于指针同样存在于指针所指对象。即使指针被另一线程修改,但可能读不到新建立的对象,因为有语句执行顺序的问题。第5章内存模型会有详细介绍。也可参考http://www.aristeia.com/Papers/DDJJulAug2004revised.pdf

c++标准委员会提供了std::once_flagstd::call_once来处理这一问题。

std::shared_ptr<some_resource> resource_ptr;
std::once_flag resource_flag; 
void init_resource()
{
    resource_ptr.reset(new some_resource); 
}
void foo()
{
    std::call_once(resource_flag,init_resource); 
    resource_ptr->do_something();
}

另一个方法是使用静态变量

class my_class;
my_class& get_my_class_instance()
{
    static my_class instance; 
    return instance;
}
3.3.2保护不常修改的数据结构

有一些数据象dns的cache,结常被读取,但很少改动。对这类应用我们可以使用一种新的mutex,reader-writer mutex。不过c++委员会拒绝了这一提议,所以我们要使用boost。

#include <map>
#include <string>
#include <mutex>
#include <boost/thread/shared_mutex.hpp>
class dns_entry;
class dns_cache
{
    std::map<std::string,dns_entry> entries;
    mutable boost::shared_mutex entry_mutex;
    public:
        dns_entry find_entry(std::string const& domain) const
        {
            boost::shared_lock<boost::shared_mutex> lk(entry_mutex);    //共享锁
            std::map<std::string,dns_entry>::const_iterator const it=entries.find(domain);
            return (it==entries.end())?dns_entry():it->second;
        }
        void update_or_add_entry(std::string const& domain,dns_entry const& dns_details)
        {
            std::lock_guard<boost::shared_mutex> lk(entry_mutex);   //排它锁
            entries[domain]=dns_details;
        }
};
3.3.3 递归锁

std::mutex只能锁定一次,试图第二次调用lock会导致末定义行为。但有时需要在同一线程内对mutex调用多次lock。为此c++标准库提供了std::recursive_mutex,它和mutex的工作方式是一样的,只是可以lock多次。如果你lock了3次,同样也需要调用unlock3次。正确的使用std::lock_guard<std::recursive_mutex>std::unique_lock<std::recursive_mutex>可以处理好这些事情。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值