为并发设计的意义何在?
在并行程序中的数据结构:要么绝对不变,要么能够正确的同步。要能够正确的同步:一种方法是设计独立的互斥量,来锁住需要保护的数据,另外一种方法就是设计一种能够并发访问的数据结构。
指导思想:设计”线程安全“的数据结构,并减少保护区域,减少序列化操作,提高并发访问的潜力。
数据结构并发设计的指导与建议(指南)
一要保证访问是安全的,二要能够真正的并发访问。根据第三章(读书笔记2)的描述须:
- 确保没有线程能够看到,数据结构的“不变量”破坏时的状态。
- 小心那些会引起条件竞争的接口,提供完整操作的函数,而非操作步骤(将步骤进行合并)。
- 注意数据结构的行为是否会产生异常,从而确保“不变量”的状态稳定。
- 将死锁的概率降到最低。使用数据结构时,需要限制锁的范围,且避免嵌套锁的存在。
第二个方面,保证真正的并发访问。作者提到了几个问题: - 锁的范围中的操作,是否允许在锁外执行?
- 数据结构中不同的区域是否能被不同的互斥量所保护?
- 所有操作都需要同级互斥量保护吗?
-
能否对数据结构进行简单的修改,以增加并发访问的概率,且不影响操作语义?
以上都是围绕指导思想展开。基于锁的并发数据结构
确保访问线程持有锁的时间最短。
线程安全栈——使用锁
线程安全栈的类定义(在读书笔记2中已经实现过):
struct empty_stack : std::exception { const char* what() const throw() { return "empty stack."; } }; template<typename T> class threadsafe_stack { private: std::stack<T> data; mutable std::mutex m; public: threadsafe_stack() {} threadsafe_stack(const threadsafe_stack& other) { std::lock_guard<std::mutex> lock(other.m); data = other.data; } threadsafe_stack& operator=(const threadsafe_stack&) = delete; void push(T new_value) { std::lock_guard<std::mutex> lock(m); data.push(std::move(new_value)); // 1 } std::shared_ptr<T> pop() { std::lock_guard<std::mutex> lock(m); if (data.empty()) throw empty_stack(); // 2 std::shared_ptr<T> const res( std::make_shared<T>(std::move(data.top()))); // 3 data.pop(); // 4 return res; } void pop(T& value) { std::lock_guard<std::mutex> lock(m); if (data.empty()) throw empty_stack(); value = std::move(data.top()); // 5 data.pop(); // 6 } bool empty() const { std::lock_guard<std::mutex> lock(m); return data.empty(); } };
构造和析构不是线程安全的,所以,用户就要保证在栈对象完成构建前,其他线程无法对其进行访问;并且,一定要保证在栈对象销毁后,所有线程都要停止对其进行访问。当用户持有锁来调用代码的时候,可能会发生死锁。此处,序列化(等待锁)线程影响了程序性能。
线程安全队列——使用锁和条件变量
使用条件变量实现的线程安全队列(读书笔记3):
template<typename T> class threadsafe_queue { private: mutable std::mutex mut; std::queue<T> data_queue; std::condition_variable data_cond; public: threadsafe_queue(){} void push(T new_value) { std::lock_guard<std::mutex> lk(mut); data_queue.push(std::move(data)); data_cond.notify_one(); // 1 } void wait_and_pop(T& value) // 2 { std::unique_lock<std::mutex> lk(mut); data_cond.wait(lk, [this] {return !data_queue.empty();}); value = std::move(data_queue.front()); data_queue.pop(); } std::shared_ptr<T> wait_and_pop() // 3 { std::unique_lock<std::mutex> lk(mut); data_cond.wait(lk, [this] {return !data_queue.empty();}); // 4 std::shared_ptr<T> res( std::make_shared<T>(std::move(data_queue.front()))); data_queue.pop(); return res; } bool try_pop(T& value) { std::lock_guard<std::mutex> lk(mut); if (data_queue.empty()) return false; value = std::move(data_queue.front()); data_queue.pop(); return true; } std::shared_ptr<T> try_pop() { std::lock_guard<std::mutex> lk(mut); if (data_queue.empty()) return std::shared_ptr<T>(); // 5 std::shared_ptr<T> res( std::make_shared<T>(std::move(data_queue.front()))); data_queue.pop(); return res; } bool empty() const { std::lock_guard<std::mutex> lk(mut); return data_queue.empty(); } };
#2与#3相对于栈的设计要好很多,它不需要线程密切关注状态(持续调用
empty()
)、对异常敏感。同时,在try_pop()
返回结果上与pop()
相比,做了改进,返回空指针和false,此时可以让线程做其他事情。如果多个线程等待push,其中一个线程被唤醒却在new时发生异常则其他都会永远沉睡。书中阐述了三种方案,一是改成notify_all()
(所有线程唤醒,其中一个获得锁,但是一旦发生异常,将重蹈覆辙),二是异常发生后再次调用notify_one()
(让另一个线程尝试完成),三是使用std::shared_ptr
实例。书中对第三种方案进行了阐释。
持有std::shared_ptr<>
实例的线程安全队列:template<typename T> class threadsafe_queue { private: mutable std::mutex mut; std::queue<std::shared_ptr<T> > data_queue; std::condition_variable data_cond; public: threadsafe_queue(){} void wait_and_pop(T& value) { std::unique_lock<std::mutex> lk(mut); data_cond.wait(lk, [this] {return !data_queue.empty();}); value = std::move(*data_queue.front()); // 1 data_queue.pop(); } bool try_pop(T& value) { std::lock_guard<std::mutex> lk(mut); if (data_queue.empty()) return false; value = std::move(*data_queue.front()); // 2 data_queue.pop(); return true; } std::shared_ptr<T> wait_and_pop() { std::unique_lock<std::mutex> lk(mut); data_cond.wait(lk, [this] {return !data_queue.empty();}); std::shared_ptr<T> res = data_queue.front(); // 3 data_queue.pop(); return res; } std::shared_ptr<T> try_pop() { std::lock_guard<std::mutex> lk(mut); if (data_queue.empty()) return std::shared_ptr<T>(); std::shared_ptr<T> res = data_queue.front(); // 4 data_queue.pop(); return res; } void push(T new_value) { std::shared_ptr<T> data( std::make_shared<T>(std::move(new_value))); // 5 std::lock_guard<std::mutex> lk(mut); data_queue.push(data); data_cond.notify_one(); } bool empty() const { std::lock_guard<std::mutex> lk(mut); return data_queue.empty(); } };
上面代码中,采用了存放
std::shared_ptr<>
实例的方式。此时,push中实例分配完毕,唤醒线程,不会发生内存分配失败(不用构造新的std::shared_ptr<>
实例,分配内存),也就不会锁在线程(继续等待push,其他线程沉睡)中。同时,内存分配的方式提升了队列工作效率,在分配内存的时候,可以让其他线程对队列进行操作。线程安全队列——使用细粒度锁和条件变量
上面使用了
std::queue<>
,使用一个互斥量对它进行保护,限制了并发访问。
队列实现——单线程版:#include <memory> template<typename T> class queue { private: struct node { T data; std::unique_ptr<node> next;//随处可见C++11的影子 node(T data_) : data(std::move(data_)) {} }; std::unique_ptr<node> head; // 1 node* tail; // 2 public: queue(){} queue(const queue& other) = delete; queue& operator=(const queue& other) = delete; std::shared_ptr<T> try_pop() { if (!head) { return std::shared_ptr<T>(); } std::shared_ptr<T> const res( std::make_shared<T>(std::move(head->data))); std::unique_ptr<node> const old_head = std::move(head); head = std::move(old_head->next); // 3 return res; } void push(T new_value) { std::unique_ptr<node> p(new node(std::move(new_value))); node* const new_tail = p.get(); if (tail) { tail->next = std::move(p); // 4 } else { head = std::move(p); // 5 } tail = new_tail; // 6 } };
多线程下的问题:当只有一个元素时,head=tail,此时try_pop和push上的是同一个锁。
通过分离数据实现并发:预分配一个虚拟节点(无数据),确保这个节点永远在队列的最后,用来分离头尾指针能访问的节点。
带有虚拟节点的队列:template<typename T> class queue { private: struct node { std::shared_ptr<T> data; // 1,数据指针 std::unique_ptr<node> next; }; std::unique_ptr<node> head; node* tail; public: queue() : head(new node), tail(head.get()) // 2,虚拟节点的创立 {} queue(const queue& other) = delete; queue& operator=(const queue& other) = delete; std::shared_ptr<T> try_pop() { if (head.get() == tail) // 3,没有元素 { return std::shared_ptr<T>(); } std::shared_ptr<T> const res(head->data); // 4,取出返回数据指针 std::unique_ptr<node> old_head = std::move(head); head = std::move(old_head->next); // 5 return res; // 6,返回数据指针 } void push(T new_value) { std::shared_ptr<T> new_data( std::make_shared<T>(std::move(new_value))); // 7,生成数据指针 std::unique_ptr<node> p(new node); //8,新虚拟节点 tail->data = new_data; // 9,虚拟节点变为新节点 node* const new_tail = p.get(); tail->next = std::move(p); tail = new_tail; } };
这里选择修改虚拟节点的数据指针来push,避免了try_pop和push对同一节点进行操作。
线程安全队列——细粒度锁版:template<typename T> class threadsafe_queue { private: struct node { std::shared_ptr<T> data; std::unique_ptr<node> next; }; std::mutex head_mutex; std::unique_ptr<node> head; std::mutex tail_mutex; node* tail; node* get_tail()//只需要一会tail,所以包装成函数获取 { std::lock_guard<std::mutex> tail_lock(tail_mutex); return tail; } std::unique_ptr<node> pop_head()//返回唯一指针(独占) { std::lock_guard<std::mutex> head_lock(head_mutex); if (head.get() == get_tail()) //只需要一会tail,不用担心别的线程之后pop(已经锁了head) { return nullptr; } std::unique_ptr<node> old_head = std::move(head); head = std::move(old_head->next); return old_head; } public: threadsafe_queue() : head(new node), tail(head.get()) {} threadsafe_queue(const threadsafe_queue& other) = delete; threadsafe_queue& operator=(const threadsafe_queue& other) = delete; std::shared_ptr<T> try_pop()//返回数据指针 { std::unique_ptr<node> old_head = pop_head(); return old_head ? old_head->data : std::shared_ptr<T>(); } void push(T new_value) { std::shared_ptr<T> new_data( std::make_shared<T>(std::move(new_value))); std::unique_ptr<node> p(new node); node* const new_tail = p.get(); //尽量减少锁的时长 std::lock_guard<std::mutex> tail_lock(tail_mutex); tail->data = new_data; tail->next = std::move(p); tail = new_tail; } };
上面封装了两个函数,极大减少了上锁时间。
下面的实现有可能会使线程中途卡住(错误实现):std::unique_ptr<node> pop_head() // 这是个有缺陷的实现 { node* const old_tail = get_tail(); // 1, 在head_mutex范围外获取旧尾节点的值 std::lock_guard<std::mutex> head_lock(head_mutex); if (head.get() == old_tail) // 2 { return nullptr; } std::unique_ptr<node> old_head = std::move(head); head = std::move(old_head->next); // 3 return old_head; }
可能在获取到旧的尾节点后,无法上锁,甚至此时链表已经发生了改变,此尾节点已不是尾节点。
下面是等待数据弹出的设计。
可上锁和等待的线程安全队列://头文件 template<typename T> class threadsafe_queue { private: struct node { std::shared_ptr<T> data; std::unique_ptr<node> next; }; std::mutex head_mutex; std::unique_ptr<node> head; std::mutex tail_mutex; node* tail; std::condition_variable data_cond; node* get_tail() { std::lock_guard<std::mutex> tail_lock(tail_mutex); return tail; } std::unique_ptr<node> pop_head() // 1 { std::unique_ptr<node> old_head = std::move(head); head = std::move(old_head->next); return old_head; } std::unique_lock<std::mutex> wait_for_data() // 2,lambda等待条件变量 { std::unique_lock<std::mutex> head_lock(head_mutex); data_cond.wait(head_lock, [&] {return head.get() != get_tail();}); return std::move(head_lock); // 3,返回锁的实例给调用者 } std::unique_ptr<node> wait_pop_head() { std::unique_lock<std::mutex> head_lock(wait_for_data()); // 4 return pop_head(); } std::unique_ptr<node> wait_pop_head(T& value) { std::unique_lock<std::mutex> head_lock(wait_for_data()); // 5 value = std::move(*head->data); return pop_head(); } std::unique_ptr<node> try_pop_head() { std::lock_guard<std::mutex> head_lock(head_mutex); if (head.get() == get_tail()) { return std::unique_ptr<node>(); } return pop_head(); } std::unique_ptr<node> try_pop_head(T& value) { std::lock_guard<std::mutex> head_lock(head_mutex); if (head.get() == get_tail()) { return std::unique_ptr<node>(); } value = std::move(*head->data); return pop_head(); } public: threadsafe_queue() : head(new node), tail(head.get()) {} threadsafe_queue(const threadsafe_queue& other) = delete; threadsafe_queue& operator=(const threadsafe_queue& other) = delete; //等待弹出 std::shared_ptr<T> wait_and_pop() { std::unique_ptr<node> const old_head = wait_pop_head(); return old_head->data; } void wait_and_pop(T& value) { std::unique_ptr<node> const old_head = wait_pop_head(value); } //尝试弹出 std::shared_ptr<T> try_pop() { std::unique_ptr<node> old_head = try_pop_head(); return old_head ? old_head->data : std::shared_ptr<T>(); } bool try_pop(T& value) { std::unique_ptr<node> const old_head = try_pop_head(value); return old_head; } void empty() { std::lock_guard<std::mutex> head_lock(head_mutex); return (head.get() == get_tail());//同类型比较 } void push(T new_value);//压入 }; template<typename T> void threadsafe_queue<T>::push(T new_value) { std::shared_ptr<T> new_data( std::make_shared<T>(std::move(new_value))); std::unique_ptr<node> p(new node); { std::lock_guard<std::mutex> tail_lock(tail_mutex); tail->data = new_data; node* const new_tail = p.get(); tail->next = std::move(p); tail = new_tail; } data_cond.notify_one();//添加通知 }
这是一个无界(unbounded)队列,线程可以持续向队列中添加数据项,即使没有元素被删除。而有界(bounded)队列一开始就已经确定了最大长度。
基于锁设计更加复杂的数据结构
栈和队列接口比较固定,而其他大多数数据结构支持更加多样化的操作。原则上,这将增大并行的可能性,但是也让对数据保护变得更加困难,因为要考虑对所有能访问到的部分。
编写一个使用锁的线程安全查询表
查询表或字典是一种类型的值(键值)和另一种类型的值进行关联(映射的方式)。一般情况下,这样的结构允许代码通过键值对相关的数据值进行查询。标准容器的接口不适合多线程进行并发访问,因为这些接口在设计的时候都存在固有的条件竞争,所以这些接口需要砍掉,进行重新修订。并发访问时,
std::map<>
接口最大的问题在于——迭代器,而许多容器给定的接口中迭代器经常使用。类似于上面,使用标准容器,一个锁来锁住全部,降低了并发的可能性。
为细粒度锁设计一个映射结构:选择哈希表(二叉树需要锁住根节点,有序数组要全部锁住,舍弃)。#include <functional>//std::hash<> #include <list> #include <vector> #include <mutex> #include <boost/thread/shared_mutex.hpp> #include <boost/thread.hpp> template<typename Key, typename Value, typename Hash = std::hash<Key> > class threadsafe_lookup_table { private: class bucket_type { private: typedef std::pair<Key, Value> bucket_value; typedef std::list<bucket_value> bucket_data; typedef typename bucket_data::iterator bucket_iterator; bucket_data data;//list<pair<Key,Value>>,链表中存放键值对 mutable boost::shared_mutex mutex; // 1,对每一个桶实施保护 bucket_iterator find_entry_for(Key const& key) const // 2,确定key是否在桶中 { return std::find_if(data.begin(), data.end(), [&](bucket_value const& item) {return item.first == key;}); } public: Value value_for(Key const& key, Value const& default_value) const { boost::shared_lock<boost::shared_mutex> lock(mutex); // 3,共享(只读)所有权 bucket_iterator const found_entry = find_entry_for(key); return (found_entry == data.end()) ? default_value : found_entry->second; } void add_or_update_mapping(Key const& key, Value const& value) { std::unique_lock<boost::shared_mutex> lock(mutex); // 4,唯一(读/写)权 bucket_iterator const found_entry = find_entry_for(key); if (found_entry == data.end()) { data.push_back(bucket_value(key, value)); } else { found_entry->second = value; } } void remove_mapping(Key const& key) { std::unique_lock<boost::shared_mutex> lock(mutex); // 5,唯一(读/写)权 bucket_iterator const found_entry = find_entry_for(key); if (found_entry != data.end()) { data.erase(found_entry); } } }; std::vector<std::unique_ptr<bucket_type> > buckets; // 6,保存桶 Hash hasher;//哈希函数 bucket_type& get_bucket(Key const& key) const // 7,可以无锁调用 { //得到索引(哪个桶) std::size_t const bucket_index = hasher(key) % buckets.size(); return *buckets[bucket_index]; } public: typedef Key key_type; typedef Value mapped_type; typedef Hash hash_type; //默认桶的数量,任意质数,哈希表在有质数个桶时,工作效率最高。 threadsafe_lookup_table( unsigned num_buckets = 19, Hash const& hasher_ = Hash()) : buckets(num_buckets), hasher(hasher_) //此处对vector初始化,大小num_buckets { for (unsigned i = 0;i<num_buckets;++i) { buckets[i].reset(new bucket_type); } } threadsafe_lookup_table(threadsafe_lookup_table const& other) = delete; threadsafe_lookup_table& operator=( threadsafe_lookup_table const& other) = delete; Value value_for(Key const& key, Value const& default_value = Value()) const { return get_bucket(key).value_for(key, default_value); // 8,可以无锁调用 } void add_or_update_mapping(Key const& key, Value const& value) { get_bucket(key).add_or_update_mapping(key, value); // 9,可以无锁调用 } void remove_mapping(Key const& key) { get_bucket(key).remove_mapping(key); // 10,可以无锁调用 } std::map<Key, Value> get_map() const; //查询表的一个“可有可无”(nice-to-have)的特性,会将选择当前状态的快照 }; template<typename Key, typename Value, typename Hash = std::hash<Key> > std::map<Key, Value> threadsafe_lookup_table<Key, Value, Hash>::get_map() const { std::vector<std::unique_lock<boost::shared_mutex> > locks;//锁的数组 for (unsigned i = 0;i<buckets.size();++i) { locks.push_back( std::unique_lock<boost::shared_mutex>(buckets[i].mutex)); //上锁后压入 } std::map<Key, Value> res; for (unsigned i = 0;i<buckets.size();++i) { for (bucket_iterator it = buckets[i].data.begin(); it != buckets[i].data.end(); ++it) { res.insert(*it);//存入键值对 } } return res; }
这个查询表作为一个整体,通过单独的操作,对每一个桶进行锁定,并且通过使用
boost::shared_mutex
允许读者线程对每一个桶进行并发访问。编写一个使用锁的线程安全链表
链表类型:访问涉及迭代器,在此,需要封装一个函数专门管理迭代器(进行控制和锁定)。
线程安全链表——支持迭代器:template<typename T> class threadsafe_list { struct node // 1 { std::mutex m; std::shared_ptr<T> data; std::unique_ptr<node> next; node() : // 2 next() {} node(T const& value) : // 3 data(std::make_shared<T>(value)) {} }; node head;//头节点 public: threadsafe_list(){} ~threadsafe_list() { remove_if([](node const&) {return true;}); } threadsafe_list(threadsafe_list const& other) = delete; threadsafe_list& operator=(threadsafe_list const& other) = delete; void push_front(T const& value) { std::unique_ptr<node> new_node(new node(value)); // 4,指向新节点的指针 std::lock_guard<std::mutex> lk(head.m); new_node->next = std::move(head.next); // 5,修改指针,unique只能move head.next = std::move(new_node); // 6 } //将风险都转移到传入的谓词(函数),交由用户决定处理 template<typename Function> void for_each(Function f) // 7,模板函数出现新的类 { node* current = &head; std::unique_lock<std::mutex> lk(head.m); // 8 while (node* const next = current->next.get()) // 9,遍历 { std::unique_lock<std::mutex> next_lk(next->m); // 10,逐个上锁 lk.unlock(); // 11,前驱节点解锁 f(*next->data); // 12,对数据处理 current = next; lk = std::move(next_lk); // 13,将锁移至前驱 } } template<typename Predicate> std::shared_ptr<T> find_first_if(Predicate p) // 14 { node* current = &head; std::unique_lock<std::mutex> lk(head.m); while (node* const next = current->next.get()) { std::unique_lock<std::mutex> next_lk(next->m); lk.unlock(); if (p(*next->data)) // 15,判别 { return next->data; // 16 } current = next; lk = std::move(next_lk); } return std::shared_ptr<T>(); } template<typename Predicate> void remove_if(Predicate p) // 17 { node* current = &head; std::unique_lock<std::mutex> lk(head.m); while (node* const next = current->next.get()) { std::unique_lock<std::mutex> next_lk(next->m); if (p(*next->data)) // 18 { std::unique_ptr<node> old_next = std::move(current->next); //取出前驱节点的后继指针 current->next = std::move(next->next); //修改前驱节点的后继指针 next_lk.unlock(); } // 20 else { lk.unlock(); // 21 current = next; lk = std::move(next_lk); } } } };