c++多线程【基于锁的数据结构】

设计基于锁的并发数据结构的关键点在于,要确保先锁定合适的互斥,再访问数据,并尽可能缩短持锁时间。

一、线程安全的queue

如下:内存操作往往是成本相当高的操作,而新的队列以安全方式为其免除了锁保护,遂缩短了互斥的持锁时长,在分配内存的时候,还运行其他线程在队列容器上执行操作,因此非常有利于增强性能。

cpp 复制代码
#include <condition_variable>
#include <mutex>
#include <queue>

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(std::shared_ptr<T>& value) {
    std::unique_lock<std::mutex> lock(mut);
    data_cond.wait(lock, [this] { return !data_queue.empty(); });
    value = std::move(*data_queue.front());
    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());
    data_queue.pop();
    return true;
  }

  std::shared_ptr<T> wait_and_pop() {
    std::unique_lock<std::mutex> lock(mut);
    // ↓ 关键:条件变量的wait方法实现阻塞
    data_cond.wait(lock, [this] { return !data_queue.empty(); });
    std::shared_ptr<T> res = data_queue.front();
    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();
    data_queue.pop();
    return res;
  }

  void push(T new_value) {
    std::shared_ptr<T> data(std::make_shared<T>(std::move(new_value)));
    std::lock_guard<std::mutex> lock(mut);
    data_queue.push(data);
    data_cond.notify_one();
  }

  bool empty() const {
    std::lock_guard<std::mutex> lk(mut);
    return data_queue.empty();
  }
};

其中wait_and_pop()函数在没有pop数据时,会阻塞线程,线程进入睡眠状态,而try_pop()函数只是判断,如果没有数据就直接返回了。用条件变量实现的睡眠是真正睡眠,不消耗CPU资源。如果实现方式如下:

cpp 复制代码
// 错误的忙等待方式(浪费CPU)
while (data_queue.empty()) {
    // 空循环,浪费CPU时间片
}

会浪费CPU的资源

二、基于精细粒度的锁和条件变量实现的线程安全的queue

如下,实现简单的加锁的单项链表:

cpp 复制代码
template <typename T>
class queue {
 private:
  struct node {
    T data;
    std::unique_ptr<node> next;
    node(T data_) : data(std::move(data_)) {}
  };

  std::unique_ptr<node> head;
  node* tail;

 public:
  queue() : tail(nullptr) {}
  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> 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);
    if (!head) tail = nullptr;
    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);
    } else {
      head = std::move(p);
    }
    tail = new_tail;
  }
};

以上最明显的问题是,Push()可以同时改动head指针和tail指针,所以该函数就需要将两个互斥都锁住。尽管这并不合适,但同时锁住这两个互斥的做法还算可行,问题不严重。

严重的问题在于,push()和try_pop()有可能并发访问同一节点的next指针:push()更新tail->next,而try_pop()则读取head->next。如果队列仅含有一项数据,即head = tail,那么head->next和tail->next两个指针的目标节点重合,而它需要保护。

如下针对以上问题新增一个head锁和tail锁,同时新增判断queue是否为空的操作,具体实现如下:

cpp 复制代码
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() {
    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()) {
      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;
  }
};

唯一需要注意的是:这种设计在队列为空时,pop操作需要同时获取head和tail锁,但这是必要的安全检查,不是风险!!!

三、基于细颗粒度锁实现的list

基本思想是让每个节点都具备自己的互斥,如果链表增长,互斥数量也会变多!这种做法的好处是,可以在链表不同部分执行真正的并发操作:每个操作仅仅需要锁住目标节点,当操作转移到下一个目标节点时,原来的锁即可解开。

cpp 复制代码
template <typename T>
class threadsafe_list {
  struct node {
    std::mutex m;
    std::shared_ptr<T> data;
    std::unique_ptr<node> next;

    node() : next() {}
    node(T const& value) : data(std::make_shared<T>(value)) {}
  };

  node head;

 public:
  threadsafe_list() {}
  threadsafe_list() {
    remove_if([](node const& n) { return true; });
  }

  threadsafe_list(threadsafl_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));
    // 只锁头节点
    std::lock_guard<std::mutex> lk(head.m);
    new_node->next = std::move(head.next);
    head.next = std::move(new_node);
  }

  template <typename Function>
  void for_each(Function f) {
    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();

      // 执行函数
      f(*next->data);
      current = next;

      // 移动所有权
      lk = std::move(next_lk);
    }
  }

  template <typename Predicate>
  std::shared_ptr<T> find_first_if(Predicate p) {
    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)) {
        return next->data;
      }

      current = next;
      lk = std::move(next_lk);
    }
    return std::shared_ptr<T>();
  }

  template <typename Predicate>
  void remove_if(Predicate p) {
    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)) {
        std::unqiue_ptr<node> old_next = std::move(current->next);
        current->next = std::move(next->next);
      } else {
        lk.unlock();
        current = next;
        lk = std::move(next_lk);
      }
    }
  }
};

手递手锁(Hand-over-hand Locking)

这是最核心的设计模式,确保在遍历链表时:

同时只锁定当前节点和下一个节点

移动时先锁定下一个节点,再释放当前节点锁

避免死锁和竞态条件

流程:

锁定当前节点

锁定下一个节点

释放当前节点锁

处理数据

移动锁所有权

相关推荐
小小晓.4 小时前
Pinely Round 4 (Div. 1 + Div. 2)
c++·算法
SHOJYS4 小时前
学习离线处理 [CSP-J 2022 山东] 部署
数据结构·c++·学习·算法
steins_甲乙4 小时前
C++并发编程(3)——资源竞争下的安全栈
开发语言·c++·安全
煤球王子5 小时前
学而时习之:C++中的异常处理2
c++
ada7_5 小时前
LeetCode(python)108.将有序数组转换为二叉搜索树
数据结构·python·算法·leetcode
仰泳的熊猫5 小时前
1084 Broken Keyboard
数据结构·c++·算法·pat考试
我不会插花弄玉5 小时前
C++的内存管理【由浅入深-C++】
c++
CSDN_RTKLIB5 小时前
代码指令与属性配置
开发语言·c++
上不如老下不如小5 小时前
2025年第七届全国高校计算机能力挑战赛 决赛 C++组 编程题汇总
开发语言·c++
雍凉明月夜5 小时前
c++ 精学笔记记录Ⅱ
开发语言·c++·笔记·vscode