基于锁的线程安全map
并发访问 std::map 和 std::unordered_map 的接口的问题在于迭代器,其他线程删除元素时会导致迭代器失效,因此 thread-safe map 的接口设计就要跳过迭代器
为了使用细粒度锁,就不应该使用标准库容器。
红关联容器数据结构
- 一是二叉树(如红黑树),但每次查找修改都要从访问根节点开始,也就表示根节点需要上锁,尽管沿着树向下访问节点时会解锁,但这个比起覆盖整个数据结构的单个锁好不了多少
- 第二种方式是有序数组,这比二叉树还差,因为无法提前得知一个给定的值应该放在哪,于是同样需要一个覆盖整个数组的锁
- 第三种方式是哈希表。假如有一个固定数量的桶,一个 key 属于哪个桶取决于 key 的属性和哈希函数,这意味着可以安全地分开锁住每个桶。如果使用读写锁,就能将并发度提高相当于桶数量的倍数
template <typename K, typename V, typename Hash = std::hash<K>>
class ConcurrentMap {
public:
// 桶数默认为 19(一般用 x % 桶数作为 x 的桶索引,桶数为质数可使桶分布均匀)
ConcurrentMap(std::size_t n = 19, const Hash& h = Hash{})
: buckets_(n), hasher_(h) {
for (auto& x : buckets_) {
x.reset(new Bucket);
}
}
V get(const K& k, const V& default_value = V{}) const {
return get_bucket(k).get(k, default_value);
}
void set(const K& k, const V& v) { get_bucket(k).set(k, v); }
void erase(const K& k) { get_bucket(k).erase(k); }
private:
struct Bucket {
std::list<std::pair<K, V>> data;
mutable std::shared_mutex m; // 每个桶都用这个锁保护
V get(const K& k, const V& default_value) const {
// 没有修改任何值,异常安全
std::shared_lock<std::shared_mutex> l(m); // 只读锁,可共享
auto it = std::find_if(data.begin(), data.end(),
[&](auto& x) { return x.first == k; });
return it == data.end() ? default_value : it->second;
}
void set(const K& k, const V& v) {
std::unique_lock<std::shared_mutex> l(m); // 写,单独占用
auto it = std::find_if(data.begin(), data.end(),
[&](auto& x) { return x.first == k; });
if (it == data.end()) {
data.emplace_back(k, v); // emplace_back 异常安全
} else {
it->second = v; // 赋值可能抛异常,但值是用户提供的,可放心让用户处理
}
}
void erase(const K& k) {
std::unique_lock<std::shared_mutex> l(m); // 写,单独占用
auto it = std::find_if(data.begin(), data.end(),
[&](auto& x) { return x.first == k; });
if (it != data.end()) {
data.erase(it);
}
}
};
Bucket& get_bucket(const K& k) const { // 桶数固定因此可以无锁调用
return *buckets_[hasher_(k) % buckets_.size()];
}
private:
std::vector<std::unique_ptr<Bucket>> buckets_;
Hash hasher_;
};
其他容器
链表:可以每个节点一把锁
struct Node {
std::mutex m; //protect this node
std::shared_ptr<T> data;
std::unique_ptr<Node> next;
Node() = default;
Node(const T& x) : data(std::make_shared<T>(x)) {}
};
队列: 头尾各一把锁
std::unique_ptr<Node> head_;
Node* tail_ = nullptr;
std::mutex head_mutex_;
mutable std::mutex tail_mutex_;
std::condition_variable cv_;