在线程间共享数据
使用线程处理并发的一个好处是在线程间共享数据更加容易。
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_lock
比std::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_flag
和std::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>
可以处理好这些事情。