第六章 基于锁的并发数据结构设计
6.1 为并发设计的意义何在?
- 设计并发数据结构,意味着多个线程可以并发的访问这个数据结构,线程可对这个数据结构做相同或不同的操作,并且每一个线程都能在自己的自治域中看到该数据结构
- 在互斥量的保护下,同一时间内只有一个线程可以获取互斥锁。互斥量为了保护数据,显式的阻止了线程对数据结构的并发访问
- 序列化(serialzation):线程轮流访问被保护的数据。这其实是对数据进行串行的访问,而非并发
- 你需要对数据结构的设计进行仔细斟酌,确保其能真正并发访问
- 减少保护区域,减少序列化操作,就能提升并发访问的潜力
6.1.1 数据结构并发设计的指导与建议(指南)
- 有两方面需要考量:一是确保访问是安全的,二是能真正的并发访问
- 安全访问:
-
确保无线程能够看到,数据结构的“不变量”破坏时的状态。
-
小心那些会引起条件竞争的接口,提供完整操作的函数,而非操作步骤。
-
注意数据结构的行为是否会产生异常,从而确保“不变量”的状态稳定。
-
将死锁的概率降到最低。使用数据结构时,需要限制锁的范围,且避免嵌套锁的存在。
不能在构造函数完成前,或析构函数完成后对数据结构进行访问
6.2 基于锁设计更加复杂的数据结构
编写一个使用锁的线程安全查询表
- 查询表或字典是一种类型的值(键值)和另一种类型的值进行关联(映射的方式)
- 查询表的使用与栈和队列不同。栈和队列上,几乎每个操作都会对数据结构进行修改,不是添加一个元素,就是删除一个,而对于查询表来说,几乎不需要什么修改
- 并发访问时,
std::map<>
接口最大的问题在于——迭代器 - 要想正确的处理迭代器,你可能会碰到下面这个问题:
当迭代器引用的元素被其他线程删除时,迭代器在这里就是个问题了; - 查询表的基本操作有:
~~~~~~~ 1) 添加一对“键值-数据”
~~~~~~~ 2) 修改指定键值所对应的数据
~~~~~~~ 3) 删除一组值
~~~~~~~ 4) 通过给定键值,获取对应数据
~~~~~~~ 5) 查询是否为空
- 之前的线程安全指导意见,例如:不要返回一个引用,并且用一个简单的互斥锁对每一个成员函数进行上锁,以确保每一个函数线程安全
- 最有可能的条件竞争在于,当一对“键值-数据”加入时;当两个线程都添加一个数据,那么肯定一个先一个后。一种方式是合并**“添加”和“修改”**操作,为一个成员函数(用于避免一个线程添加的新key,是下一个线程要添加的新key)
- 从接口角度看,有一个问题很是有趣,那就是任意(if any)部分获取相关数据:
- 一种选择是允许用户提供一个“默认”值,在键值没有对应值的时候进行返回
mapped_type get_value(key_type const& key, mapped_type default_value);
- 可以扩展成返回一个
std::pair<mapped_type, bool>
来代替mapped_type
实例,其中bool代表返回值是否是当前键对应的值 - 另一个选择是,返回一个有指向数据的智能指针;当指针的值是NULL时,那么这个键值就没有对应的数据
- 但是在同一时间内,也只有一个线程能对数据结构进行修改
为细粒度锁设计一个映射结构
- 为了允许细粒度锁能正常工作,需要对于数据结构的细节进行仔细的考虑,而非直接使用已存在的容器,例如
std::map<>
。 - 这里列出三个常见关联容器的方式:
-
二叉树,比如:红黑树
-
有序数组
-
哈希表
- 二叉树的方式,不会对提高并发访问的概率;每一个查找或者修改操作都需要访问根节点,因此,根节点需要上锁。虽然,访问线程在向下移动时,这个锁可以进行释放,但相比横跨整个数据结构的单锁,并没有什么优势。
- 有序数组是最坏的选择,因为你无法提前言明数组中哪段是有序的,所以你需要用一个锁将整个数组锁起来。
- 哈希表: 假设有固定数量的桶,每个桶都有一个键值(关键特性),以及散列函数。这就意味着你可以安全的对每个桶上锁;当你再次使用互斥量(支持多读者单作者)时,你就能将并发访问的可能性增加N倍,这里N是桶的数量
缺点:对于键值的操作,需要有合适的函数
线程安全的查询表代码实现
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;
mutable boost::shared_mutex mutex; // 1
bucket_iterator find_entry_for(Key const& key) const // 2
{
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_)
{
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
}
};