《c++ concurrency in action》读书笔记3--线程间数据共享
一 线程间数据共享的问题
1. 线程间数据共享带来了很多好处,但同时也有很多问题。而问题的根源在于对数据的修改. 在并行编程中,一个常见的场景就时:条件竞争( race condition);
2. 避免条件竞争的方法: a. 保护机制 b. 将change变为不可分割的changes (lock-free programming) c. 将数据更新作为一个事务( transaction)
二 利用互斥保护共享数据
2.1. 通过std::mutex创建一个互斥对象,通过调用lock()函数实现加锁,unlock()实现放锁。在实际使用中不建议直接调用这两个函数,如果直接调用这两个函数需要确保所有路径都必须确保unlock,包括有异常抛出的情况。c++函数库提供了std::lock_guard,为互斥对象实现RAII
#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(); }
2.2. 需要注意的是,如果一个函数返回保护数据的引用或指针或做为out参数,被保护数据则会脱离锁的保护,所以对于互斥锁保护的数据,在设计接口时一定要小心。
同时,将被保护数据的引用或者指针作为参数传递给其他函数也时十分危险的,这些函数可能会存储被保护数据的引用或者指针,让其脱离锁的保护。如下面这个例子:
class some_data { int a; std::string b; public: void do_something(); }; class data_wrapper { private: some_data data; std::mutex m; public: template<typename Function> void process_data(Function func) { std::lock_guard<std::mutex> l(m); func(data); } }; some_data* unprotected; // 这个数据不在受保护。 void malicious_function(some_data& protected_data) { unprotected=&protected_data; } data_wrapper x; void foo() { x.process_data(malicious_function); unprotected->do_something(); }
所以一个常用的原则是: 不要将被保护数据的引用或指针传递到锁的范围外。即不要将其做为返回值, OUT对象,或者将其传递到用户定义的函数中。
2.3 接口间的竞态条件
在多线程中,接口的设计也要十分的小心,比如设计了一个stack,并保证每个函数都是线程安全的,但如下代码中,empty返回真,按其他线程可能会先调用了top, 所以,empty和top之间还是存在竞态条件。
stack<int> s; if(!s.empty()) { int const value=s.top(); s.pop(); do_something(value); }
同时,上面代码还隐藏着一个更难发现的问题(一个数被读了两次,另一个数直接被丢弃了):
2.4 死锁
1. 一个避免死锁的有效做法是以相同顺序给两个互斥变量加锁,但这也不能完全适用,比如在一个交换两个变量内容的函数中, swap(A, B)中 swap(B, A) 同时调用仍然可能会造成死锁。
2. 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) return; std::lock(lhs.m,rhs.m); std::lock_guard<std::mutex> lock_a(lhs.m,std::adopt_lock); std::lock_guard<std::mutex> lock_b(rhs.m,std::adopt_lock); swap(lhs.some_detail,rhs.some_detail); } };
std::adopt_lock参数说明互斥量已经加锁了,只是适配已有锁的所有权。 std::lock提供了all-or-nothing的语义。
2.5 避免死锁的建议
有很多情况会造成死锁,比如两个线程,分别调用对方的join()函数,则可能会造成死锁。所以,想避免死锁应该注意以下几点:
1. 如果你已经持有锁后,不要在去拿其他锁。如果你需要多把锁,应该使用std::lock.
2. 避免在持有锁的情况下,调用用户提供的代码。
3. 如果你需要多把锁,而由于某种原因不能用std::lock,应保证在每个线程中拿锁的顺序一致。
4. 使用层次锁
层次锁是按顺序拿锁的一个特例,通过代码的方式保证只有比当前低的层次才能拿到锁,如下是一个层次锁的实现:
class hierarchical_mutex { std::mutex internal_mutex; unsigned long const hierarchy_value; unsigned long previous_hierarchy_value; static thread_local unsigned long this_thread_hierarchy_value; void check_for_hierarchy_violation() { if(this_thread_hierarchy_value <= hierarchy_value) { throw std::logic_error(“mutex hierarchy violated”); } } void update_hierarchy_value() { previous_hierarchy_value=this_thread_hierarchy_value; this_thread_hierarchy_value=hierarchy_value; } public: explicit hierarchical_mutex(unsigned long value): hierarchy_value(value), previous_hierarchy_value(0) {} void lock() { check_for_hierarchy_violation(); internal_mutex.lock(); update_hierarchy_value(); } void unlock() { this_thread_hierarchy_value=previous_hierarchy_value; internal_mutex.unlock(); } bool try_lock() { check_for_hierarchy_violation(); if(!internal_mutex.try_lock()) return false; update_hierarchy_value(); return true; } }; thread_local unsigned long hierarchical_mutex::this_thread_hierarchy_value(ULONG_MAX);
thread_local保证了每个线程都有一个独立的当前层次值,check_for_hierarchy_violation 保证了只有更低层次才能拿到锁。
2.6 std::unique_lock的使用
std::unique_lock比std::lock_guard更加的灵活,利用std::unique_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) 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); }
2.7 互斥所有权转移
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(); }
std::unique_lock, 只能用move操作
2.8 std::unique_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); }
三、其他一些保护共享数据的方法
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 locking
void undefined_behaviour_with_double_checked_locking() { if(!resource_ptr) { std::lock_guard<std::mutex> lk(resource_mutex); if(!resource_ptr) { resource_ptr.reset(new some_resource); } } resource_ptr->do_something(); }
这种方法存在一个问题:if(!resource_ptr)和resource_ptr.reset(new some_resource)存在条件竞争,即存在resource_ptr虽然不为null,但并不能看到新创建的some_resource实例,导致调用do_domething()出错. 这种行为是未定义的。
方法三:std::once_flag和std::call_once
call_once的开销比mutex要小,特别是初始化已经完成的情况。
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(); }
call_once封装到一个类中:
class X { private: connection_info connection_details; connection_handle connection; std::once_flag connection_init_flag; void open_connection() { connection=connection_manager.open(connection_details); } public: X(connection_info const& connection_details_): connection_details(connection_details_) {} void send_data(data_packet const& data) { std::call_once(connection_init_flag,&X::open_connection,this); connection.send_data(data); } data_packet receive_data() { std::call_once(connection_init_flag,&X::open_connection,this); return connection.receive_data(); } };
注意std::mutex std::once_flag都是不能被拷贝或者move的。
方法四:使用静态变量
class my_class; my_class& get_my_class_instance() { static my_class instance; return instance; }
在c++11后,static变量的初始化会保证是线程安全的。
2. 读写锁
利用boost::shared_mutex 可以实现读写锁,boost::shared_lock<boost::shared_mutex> 共享访问,std::lock_guard<boost::shared_mutex>互斥访问。
#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. 递归锁
std::recursive_mutex可以在同一个线程中被多次加锁,但必须保证lock的次数和unlock的次数相同,才能保证其他线程可以被使用。