C++ 并发编程实战 第六章 设计基于锁的并发数据结构

目录

6.1 并发设计的内涵

6.2 基于锁的并发数据结构

6.2.1 采用锁实现线程安全的栈容器

6.2.2 采用锁和条件变量实现线程安全的队列容器

6.2.3 采用精细粒度的锁和条件变量实现线程安全的队列容器

6.3 设计更复杂的基于锁的并发数据结构

6.3.1 采用锁编写线程安全的查找表

6.3.2 采用多种锁编写线程安全的链表、


参考:https://github.com/xiaoweiChen/CPP-Concurrency-In-Action-2ed-2019/blob/master/content/chapter5/5.0-chinese.md

6.1 并发设计的内涵

设计并发数据结构是为了让多线程并发访问,并且线程可对数据结构做相同或不同的操作。多线程环境下,无数据丢失和损毁,所有的数据需要维持原样,且无条件竞争的数据结构,称之为“线程安全”的数据结构。通常情况下,多个线程对数据结构进行并发操作是安全的,但不同操作需要单线程独立访问数据结构。当线程执行不同的操作时,对同一数据结构的并发操作是安全的,而多线程执行同样的操作时,可能会出现问题。

实际的设计意义并不止上面提到的那样,而是要为线程提供并发访问数据结构的机会。本质上,在互斥量的保护下同一时间内只有一个线程可以获取锁。互斥量为了保护数据,会显式阻止线程对数据结构的并发访问。

串行化(serialzation)则是线程轮流访问数据,对数据进行串行访问。因此,需要对数据结构仔细斟酌,确保能进行真正的并发。虽然,有些数据结构比其他结构的并发访问范围更大,但思路都是一样的:减少保护区域,减少序列化操作,提升并发访问的能力。

6.2 基于锁的并发数据结构

基于锁的并发数据结构需要确保访问线程持有锁的时间最短,对于只有一个互斥量的数据结构来说十分困难。需要锁之外的操作不能访问数据,保证不会产生条件竞争。使用多个互斥量保护数据结构不同的区域时,问题会更加明显。当操作需要获取多个互斥锁时,可能会产生死锁,所以使用多个互斥量时要格外小心。

6.2.1 采用锁实现线程安全的栈容器

代码6.1 线程安全栈的类定义

#include 

struct empty_stack: std::exception
{
  const char* what() const throw();
};

template
class threadsafe_stack
{
private:
  std::stack data;
  mutable std::mutex m;
public:
  threadsafe_stack(){}
  threadsafe_stack(const threadsafe_stack& other)
  {
    std::lock_guard lock(other.m);
    data=other.data;
  }

  threadsafe_stack& operator=(const threadsafe_stack&) = delete;

  void push(T new_value)
  {
    std::lock_guard lock(m);
    data.push(std::move(new_value));  // 1
  }
  std::shared_ptr pop()
  {
    std::lock_guard lock(m);
    if(data.empty()) throw empty_stack();  // 2
    std::shared_ptr const res(
      std::make_shared(std::move(data.top())));  // 3
    data.pop();  // 4
    return res;
  }
  void pop(T& value)
  {
    std::lock_guard lock(m);
    if(data.empty()) throw empty_stack();
    value=std::move(data.top());  // 5
    data.pop();  // 6
  }
  bool empty() const
  {
    std::lock_guard lock(m);
    return data.empty();
  }
};

6.2.2 采用锁和条件变量实现线程安全的队列容器

代码6.2 使用条件变量实现的线程安全队列

template
class threadsafe_queue
{
private:
  mutable std::mutex mut;
  std::queue data_queue;
  std::condition_variable data_cond;

public:
  threadsafe_queue()
  {}

  void push(T data)
  {
    std::lock_guard lk(mut);
    data_queue.push(std::move(data));
    data_cond.notify_one();  // 1
  }

  void wait_and_pop(T& value)  // 2
  {
    std::unique_lock lk(mut);
    data_cond.wait(lk,[this]{return !data_queue.empty();});
    value=std::move(data_queue.front());
    data_queue.pop();
  }

  std::shared_ptr wait_and_pop()  // 3
  {
    std::unique_lock lk(mut);
    data_cond.wait(lk,[this]{return !data_queue.empty();});  // 4
    std::shared_ptr res(
      std::make_shared(std::move(data_queue.front())));
    data_queue.pop();
    return res;
  }

  bool try_pop(T& value)
  {
    std::lock_guard lk(mut);
    if(data_queue.empty())
      return false;
    value=std::move(data_queue.front());
    data_queue.pop();
    return true;
  }

  std::shared_ptr try_pop()
  {
    std::lock_guard lk(mut);
    if(data_queue.empty())
      return std::shared_ptr();  // 5
    std::shared_ptr res(
      std::make_shared(std::move(data_queue.front())));
    data_queue.pop();
    return res;
  }

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

异常安全会有一些变化,不止一个线程等待对队列进行推送操作时,只会有一个线程因data_cond.notify_one()而继续工作。但是,如果工作线程在wait_and_pop()中抛出一个异常,例如:构造新的std::shared_ptr<>对象④时抛出异常,那么其他线程则会永世长眠。这种情况不可以,所以调用函数需要改成data_cond.notify_all(),这个函数将唤醒所有的工作线程,不过当大多线程发现队列依旧是空时,又会耗费资源让线程重新进入睡眠。第二种替代方案,有异常抛出时,让wait_and_pop()函数调用notify_one(),从而让个另一个线程去索引存储的值。第三种替代方案,将std::shared_ptr<>的初始化过程移到push()中,并且存储std::shared_ptr<>实例,而不是直接使用数据值,将std::shared_ptr<>拷贝到内部std::queue<>中就不会抛出异常了,这样wait_and_pop()又是安全的了。下面的代码,就是根据第三种方案修改的。

代码6.3 持有std::shared_ptr<>实例的线程安全队列

template
class threadsafe_queue
{
private:
  mutable std::mutex mut;
  std::queue > data_queue;
  std::condition_variable data_cond;
public:
  threadsafe_queue()
  {}

  void wait_and_pop(T& value)
  {
    std::unique_lock 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 lk(mut);
    if(data_queue.empty())
      return false;
    value=std::move(*data_queue.front());  // 2
    data_queue.pop();
    return true;
  }

  std::shared_ptr wait_and_pop()
  {
    std::unique_lock lk(mut);
    data_cond.wait(lk,[this]{return !data_queue.empty();});
    std::shared_ptr res=data_queue.front();  // 3
    data_queue.pop();
    return res;
  }

  std::shared_ptr try_pop()
  {
    std::lock_guard lk(mut);
    if(data_queue.empty())
      return std::shared_ptr();
    std::shared_ptr res=data_queue.front();  // 4
    data_queue.pop();
    return res;
  }

  void push(T new_value)
  {
    std::shared_ptr data(
    std::make_shared(std::move(new_value)));  // 5
    std::lock_guard lk(mut);
    data_queue.push(data);
    data_cond.notify_one();
  }

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

6.2.3 采用精细粒度的锁和条件变量实现线程安全的队列容器

代码6.6 线程安全队列——细粒度锁版

template
class threadsafe_queue
{
private:
  struct node
  {
    std::shared_ptr data;
    std::unique_ptr next;
  };
  std::mutex head_mutex;
  std::unique_ptr head;
  std::mutex tail_mutex;
  node* tail;

  node* get_tail()
  {
    std::lock_guard tail_lock(tail_mutex);
    return tail;
  }

  std::unique_ptr pop_head()
  {
    std::lock_guard head_lock(head_mutex);
    if(head.get()==get_tail())
    {
      return nullptr;
    }
    std::unique_ptr 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 try_pop()
  {
     std::unique_ptr old_head=pop_head();
     return old_head?old_head->data:std::shared_ptr();
  }

  void push(T new_value)
  {
    std::shared_ptr new_data(
      std::make_shared(std::move(new_value)));
    std::unique_ptr p(new node);
    node* const new_tail=p.get();
    std::lock_guard tail_lock(tail_mutex);
    tail->data=new_data;
    tail->next=std::move(p);
    tail=new_tail;
  }
};

6.3 设计更复杂的基于锁的并发数据结构

6.3.1 采用锁编写线程安全的查找表

代码6.11 线程安全的查询表

template >
class threadsafe_lookup_table
{
private:
  class bucket_type
  {
  private:
    typedef std::pair bucket_value;
    typedef std::list bucket_data;
    typedef typename bucket_data::iterator bucket_iterator;

    bucket_data data;
    mutable std::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
    {
      std::shared_lock 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 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 lock(mutex);  // 5
      bucket_iterator const found_entry=find_entry_for(key);
      if(found_entry!=data.end())
      {
        data.erase(found_entry);
      }
    }
  };

  std::vector > 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

6.3.2 采用多种锁编写线程安全的链表、

提供了这些操作,链表才能为通用容器,这将帮助我们添加更多功能,比如:指定位置上插入元素,不过这对于查询表来说就没有必要了,所以算是给读者们留的一个作业吧。

代码6.13 线程安全链表——支持迭代器(包含我写的指定位置插入---丐版)

#ifndef THREADSAFE_LIST_H_
#define THREADSAFE_LIST_H
#include
#include
template
class threadsafe_list
{
  struct node  // 1
  {
    std::mutex m;
    std::shared_ptr data;
    std::unique_ptr next;
    node():  // 2
      next()
    {}

    node(T const& value):  // 3
      data(std::make_shared(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 new_node(new node(value));  // 4
    std::lock_guard lk(head.m);
    new_node->next=std::move(head.next);  // 5
    head.next=std::move(new_node);  // 6
  }

  template
  void for_each(Function f)  // 7
  {
    node* current=&head;
    std::unique_lock lk(head.m);  // 8
    while(node* const next=current->next.get())  // 9
    {
      std::unique_lock next_lk(next->m);  // 10
      lk.unlock();  // 11
      f(*next->data);  // 12
      current=next;
      lk=std::move(next_lk);  // 13
    }
  }

  template
  std::shared_ptr find_first_if(Predicate p)  // 14
  {
    node* current=&head;
    std::unique_lock lk(head.m);
    while(node* const next=current->next.get())
    {
      std::unique_lock 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();
  }

  template
  void remove_if(Predicate p)  // 17
  {
    node* current=&head;
    std::unique_lock lk(head.m);
    while(node* const next=current->next.get())
    {
      std::unique_lock next_lk(next->m);
      if(p(*next->data))  // 18
      {
        std::unique_ptr 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);
      }
    }
  }

  void insert_in_position(T const& value,const int& position){
  	std::unique_ptr new_node(new node(value));
	node* current=&head;
	node* next = current->next.get();

	std::unique_lock lk(head.m);
	for(int i = 0; i next_lk(next->m);
		lk.unlock();
		current = next;
		lk = std::move(next_lk);
		next = current->next.get();
	}
	lk.unlock();
	
	std::unique_lock cur_lk(current->m);
	new_node->next = std::move(current->next);
	current->next = std::move(new_node);
  }
};

#endif

你可能感兴趣的:(数据结构,c++,C++并发编程,开发语言)