C++并发编程(CH03)[用mutex来保护共享数据-01&02]

Problems with sharing data between threads

  1. 如果多个线程只是并发的读数据.这不会导致竞争.

Race conditions

  1. 有的写竞争也不会导致严重的问题.比如往同一个队列中push对象的时候.谁先谁后其实不重要.
  2. 但是为了修改链表的竞争就非常严重了.会导致不明确行为.

Avoiding problematic race conditions

  1. 加锁机制.

  2. 无锁编程.非常高级且复杂的方法.

  3. 采用software transactional
    memory,也就是对操作记录log的方式。然后要么一次性执行要么不执行。本书不包含字部分内容,比如A和B同时在修改,不同副本。但是A提交在先。那么B会读取日志文件。然后在稍后的操作中根据A的修改再次执行B的操作)

Protecting shared data with mutexes

Using mutexes in C++

直接使用mutex的lock和unlock不是太好,因为你很可能忘记unlock.所以最好使用std::lock_guard,它可以自动的在函数体内加锁和退出时的解锁。

#include <list>
#include <mutex>
#include <algorithm>
std::list<int> some_list;                       
std::mutex some_mutex;                          
void add_to_list(int new_value)
{
  std::lock_guard<std::mutex> guard(some_mutex);
  some_list.push_back(new_value);
}
bool list_contains(int value_to_find)
{
  std::lock_guard<std::mutex> guard(some_mutex);
  return std::find(some_list.begin(),some_list.end(),value_to_find)
    != some_list.end();
}

其中C++17提供了class template argument deduction.这样模板可以这样写.

std::lock_guard guard(some_mutex);

上面的代码仅仅是示例.最好不要把数据和mutex放在global变量区.应该用class把它们抱起来.隔离数据.

不要把被保护起来的共享变量通过函数返回值的形式暴露给外部使用(reference
or pointer)。这样就打破了竞争资源的保护现场。

Structuring code for protecting shared data

既不要把共享保护数据传给外部是用,也不要把他们传入不信任的函数中,让他们处理。因为他们可能将这些数据的指针和引用保存起来。

class some_data
{
  int a;
  std::string b;
public:
  void do_something();
};
class data_wrapper
{
private:
  some_data data;
  std::mutex m;
public:
  template<typename Function>
  void process_data(Function func)
  {
    std::lock_guard<std::mutex> l(m);
    func(data);                       //<--Pass “protected” data to user-supplied function
  }
};
some_data* unprotected;
void malicious_function(some_data& protected_data)
{
  unprotected=&protected_data;
}
data_wrapper x;
void foo()
{
  x.process_data(malicious_function); //<--Pass in a malicious function
  unprotected->do_something();        //<--Unprotected access to protected data
}

编程指南谨记, Don’t pass pointers and references to protected data
outside the scope of the lock, whether by returning them from a
function, storing them in externally visible memory, or passing them as
arguments to user-supplied functions
)

Spotting race conditions inherent in interfaces

  1. 注意,如果你枷锁不对.也会出现竞争问题.比如双链表删除操作必须要锁住3个节点,才可以.
  2. 即便是你锁住了整个结构,也会出现问题,加入数据结构在设计上,返回了引用.问题也会存在.这是设计数据结构人员的错误.在这种设计下,即便是无锁编程也会导致错误.

又如下面的stack实现,他有一些问题.

template<typename T,typename Container=std::deque<T> >
class stack
{
public:
  explicit stack(const Container&);
  explicit stack(Container&& = Container());
  template <class Alloc> explicit stack(const Alloc&);
  template <class Alloc> stack(const Container&, const Alloc&);
  template <class Alloc> stack(Container&&, const Alloc&);
  template <class Alloc> stack(stack&&, const Alloc&);
  bool empty() const;
  size_t size() const;
  T& top();
  T const& top() const;
  void push(T const&);
  void push(T&&);
  void pop();
  void swap(stack&&);
};
  1. 其中empty() and
    size()只能保证在调用返回的一刻是对的.一旦返回,另外的线程可能回去添加新元素或者删除元素.这是多线程编程的特点.当然如果是单线程,这些问题就不存在了.

    stack<int> s;
    if(!s.empty())
    {
        int const value=s.top(); // 在一个空的stack上调用top是未定义行为.如果你是单线程,在一个空的stack里面,调用top().你会造成segmentation fault.
        s.pop();
        do_something(value);
    }
    //(sai:如果并发执行这段代码则会出现问题)
    

    如何解决?

    • 重新设计stack的函数.在非法情况抛出异常.这种设计要求用户去捕捉异常.

    • 上面的代码在下面的多线程情况下.会发生问题

      Thread A                                   Thread B
      if(!s.empty())                   
                                              if(!s.empty())
      
         int const value=s.top();         
                                                 int const value=s.top();
      
         s.pop();                         
         do_something(value);                     s.pop();
                                                  do_something(value);
      

      函数 do_something
      操作的值是同一个value.是否是程序正确的行为.要依赖于
      do_something 的行为是否正确.

  2. stack之所以实现一个top和一个pop是因为如果pop执行返回值和删除操作。那么如果一个顶元素很大,当执行复制拷贝的时候,如果发生异常。则会造成数据丢失,因为pop执行了两个操作,返回值和删除值。所以实现top和pop来保护数据没那么容易丢失.

  3. 正是这个top
    pop的分离设计导致了上面提到的问题.虽然你可以有下面的方法来克服,但是都需要付出代价.

解决方法

  1. OPTION 1: PASS IN A REFERENCE

    实现线程安全的pop并且pop返回值,并弹出对象.可以用引用作为参数传递.

    std::vector<int> result;
    some_stack.pop(result);
    

    缺点:

    1. 你必须提前声明一个变量result然后再来引用住对象,比如result.
    2. 另外,构建临时对象可能非常耗时间.
    3. 构建临时对象可能需要某些参数,但这时已经很难访问这些参数了.
    4. 而且要求对象是可赋值的.有些用户类型可能不允许赋值.
  2. OPTION 2: REQUIRE A NO-THROW COPY CONSTRUCTOR OR MOVE CONSTRUCTOR

    定义不抛出异常的拷贝构造函数和复制构造函数

    注意:

    1. 你可以使用 std::is_nothrow_copy_constructible and
      std::is_nothrow_move_constructible
      来判断类是否含有拷贝构造函数和移动构造函数是否是不抛出异常的.
    2. 主要缺点是:实际上很多类并没有这些构造函数.
  3. OPTION 3: RETURN A POINTER TO THE POPPED ITEM

    实现线程安全的pop,使用指针传递数据.

    缺点:

    在运行期动态开辟内存在效率上不高.

  4. OPTION 4: PROVIDE BOTH OPTION 1 AND EITHER OPTION 2 OR 3

    实现线程安全的pop,提供以上三种给用户,让用户选择)

  5. EXAMPLE DEFINITION OF A THREAD-SAFE STACK

    (std::shared_ptr<>在最后一个引用计数消失之时,进行析构

    #include <exception>
    #include <memory>                                                //<--For std::shared_ptr<>
    struct empty_stack: std::exception
    {
      const char* what() const throw();
    };
    template<typename T>
    class threadsafe_stack
    {
    public:
      threadsafe_stack();
      threadsafe_stack(const threadsafe_stack&);
      threadsafe_stack& operator=(const threadsafe_stack&) = delete; //<--1 Assignment operator is deleted
      void push(T new_value);
      std::shared_ptr<T> pop();
      void pop(T& value);
      bool empty() const;
    };
    

    (线程安全的stack实现)

    #include <exception>
    #include <memory>
    #include <mutex>
    #include <stack>
    struct empty_stack: std::exception
    {
      const char* what() const throw();//(sai:const throw()是异常规格说明,如果()内为空则表示不再抛出异常,为int时,表示可以抛出int的异常。)
    };
    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;                                               //<--1 Copy performed in constructor body
      }
      threadsafe_stack& operator=(const threadsafe_stack&) = delete;
      void push(T new_value)
      {
        std::lock_guard<std::mutex> lock(m);
        data.push(new_value);
      }
      std::shared_ptr<T> pop()
      {
        std::lock_guard<std::mutex> lock(m);
        if(data.empty()) throw empty_stack();                          //<--Check for empty before trying to pop value
        std::shared_ptr<T> const res(std::make_shared<T>(data.top())); //<--Allocate return value before modifying stack
        data.pop();
        return res;
      }
      void pop(T& value)
      {
        std::lock_guard<std::mutex> lock(m);
        if(data.empty()) throw empty_stack();
        value=data.top();
        data.pop();
      }
      bool empty() const
      {
        std::lock_guard<std::mutex> lock(m);
        return data.empty();
      }
    };
    

Deadlock: the problem and a solution

注意:

  1. 当一次性要锁住多个mutex的时候,记住按照相同的顺序来锁定就不会发生死锁问题.但是这种做法,很难维护.如果你疏忽或者别人不知道.那么很容易就改变锁定顺序.造成死锁问题.
  2. C++提供了一次锁定多个mutex的操作.std::lock.这个操作不会造成死锁.
class some_big_object;
void swap(some_big_object& lhs,some_big_object& rhs);
class X
{
private:
  some_big_object some_detail;
  std::mutex m;
public:
  X(some_big_object const& sd):some_detail(sd){}
  friend void swap(X& lhs, X& rhs)
  {
    if(&lhs==&rhs)
      return;
    std::lock(lhs.m,rhs.m);                                    //<--1
    //(一次性把多个mutex锁住)
    std::lock_guard<std::mutex> lock_a(lhs.m,std::adopt_lock); //<--2
    std::lock_guard<std::mutex> lock_b(rhs.m,std::adopt_lock); //<--3
    //(std::adopt_lock 参数.可以告诉std::lock_guard不必加锁,因为锁已经加好了)因为前面已经用lock把两个都锁住了.
    swap(lhs.some_detail,rhs.some_detail);
  }
};

C++17提供的一个scoped_lock,可以一次性锁住所有不同类型mutex,和多个mutex.下面是重写的代码示例.

void swap(X& lhs, X& rhs)
{
    if(&lhs==&rhs)
        return;
    std::scoped_lock guard(lhs.m,rhs.m); //注意这里使用了C++17的模板类型自动推导.
    swap(lhs.some_detail,rhs.some_detail);
}

自动模板类型推导的等效结果.

std::scoped_lock<std::mutex,std::mutex> guard(lhs.m,rhs.m);

即便由上面的这些机制,但是死锁是无法避免的.你需要对自己的代码有所警觉.

Further guidelines for avoiding deadlock

如果两个线程互相调用join等待对方完成,则也构成一个死锁。多个线程只要join调用构成一个环也会构成死锁。避免此种现象的指导原则是不要等待一个
存在潜在威胁 的线程.

AVOID NESTED LOCKS

不要嵌套使用mutex

AVOID CALLING USER-SUPPLIED CODE WHILE HOLDING A LOCK

不要调用用户的代码,因为用户代码可能会去获取mutex这样就可能产生deadlock.

ACQUIRE LOCKS IN A FIXED ORDER

  1. 如果不能同时对mutex加锁,那么最好在不同线程中按相同顺序加锁
  2. 比如双向链表.你很难保证删除操作的锁定顺序.因为两个方向的遍历都可以发生.有一个办法就是限制整个list的遍历.只允许一个方向.这样锁定顺序就对了.

USE A LOCK HIERARCHY

如果你要实现带等级结构的mutex.如下.
那么C++是没有提供这些东西的.你需要自己实现.

hierarchical_mutex high_level_mutex(10000);                 //<--1
hierarchical_mutex low_level_mutex(5000);                   //<--2
int do_low_level_stuff();
int low_level_func()                                   
{
  std::lock_guard<hierarchical_mutex> lk(low_level_mutex);  //<--3
  return do_low_level_stuff();
}

void high_level_stuff(int some_param);
void high_level_func()                                 
{
  std::lock_guard<hierarchical_mutex> lk(high_level_mutex); //<--4
  high_level_stuff(low_level_func());                       //<--5
}                                          
void thread_a()                                             //<--6
{
  high_level_func();
}
hierarchical_mutex other_mutex(100);                        //<--7
void do_other_stuff();
void other_stuff()                                     
{
  high_level_func();                                        //<--8
  do_other_stuff();
}
void thread_b()                                             //<--9
{
  std::lock_guard<hierarchical_mutex> lk(other_mutex);      //<--10
  other_stuff();
}

实现代码 :
自己实现的mutex,需要提供三个借口就可以用RAII管理器管理起来了.(lock,unlock,
try_lock)

class hierarchical_mutex                                  
{
  std::mutex internal_mutex;
  unsigned long const hierarchy_value;
  unsigned long previous_hierarchy_value;
  static thread_local unsigned long this_thread_hierarchy_value; //<--1

  void check_for_hierarchy_violation()                    
  {                                                       
    if(this_thread_hierarchy_value <= hierarchy_value)           //<--2
      {
        throw std::logic_error("mutex hierarchy violated");
      }
  }
  void update_hierarchy_value()
  {
    previous_hierarchy_value=this_thread_hierarchy_value;        //<--3
    this_thread_hierarchy_value=hierarchy_value;
  }
public:
  explicit hierarchical_mutex(unsigned long value):
    hierarchy_value(value),
    previous_hierarchy_value(0)
  {}
  void lock()
  {
    check_for_hierarchy_violation();
    internal_mutex.lock();                                       //<--4
    update_hierarchy_value();                                    //<--5
  }
  void unlock()
  {
    this_thread_hierarchy_value=previous_hierarchy_value;        //<--6
    internal_mutex.unlock();                              
  }
  bool try_lock()
  {
    check_for_hierarchy_violation();                      
    if(!internal_mutex.try_lock())                               //<--7
      return false;
    update_hierarchy_value();
    return true;
  }
};

thread_local unsigned long
  hierarchical_mutex::this_thread_hierarchy_value(ULONG_MAX);    //<--8

thread_local 表示这个变量再不同线程之中是独立的.

EXTENDING THESE GUIDELINES BEYOND LOCKS

  1. 在获取到了锁之后,不要建议等待其他线程。
  2. 最好在调用线程的地方原地就等待线程结束.
  3. 如果实在等级化的mutex下面等待线程,那么相同等级的线程可以等待.不同等级的就不要等待了,如果等待肯定出错.

Flexible locking with std::unique_lock

前面提到的lock_guard操作非常受限,你不能中途unlock.需要等到作用域结束.如果你想提前结束锁定等.或者在构造函数的地方不想锁定.你需要
unique_lock.

class some_big_object;
void swap(some_big_object& lhs,some_big_object& rhs);
class X
{
private:
  some_big_object some_detail;
  std::mutex m;
public:
  X(some_big_object const& sd):some_detail(sd){}
  friend void swap(X& lhs, X& rhs)
  {
    if(&lhs==&rhs)
      return;
    std::unique_lock<std::mutex> lock_a(lhs.m,std::defer_lock); // std::defer_lock的作用就是,在构造函数这里不进行锁定.后面你需要手动来锁定.
    std::unique_lock<std::mutex> lock_b(rhs.m,std::defer_lock); 
    std::lock(lock_a,lock_b);                                   // 实际锁定的地方.
    //(sai:unique_lock可以延迟枷锁,可以直接传递给std::lock函数,因为unique_lock函数提供了对应接口)
    swap(lhs.some_detail,rhs.some_detail);
  }
};
  1. unique_lock更为灵活,但是效率有所牺牲。最好用lock_guard。unique_lock用在延迟枷锁的情形比如上面例子提到的情形
  2. 如果你想转移mutex所有权,你应该使用unique_lock.
  3. 如果你可以使用C++17你可以用std::scrope_lock来替换 unique_lock.

Transferring mutex ownership between scopes

std::unique_lock 是movable但是不能copyable.
但是lock_guard是既不能movable也不能copyable.

std::unique_lock<std::mutex> get_lock()
{
  extern std::mutex some_mutex;
  std::unique_lock<std::mutex> lk(some_mutex);
  prepare_data();
  return lk;                                   //<--1
}
void process_data()
{
  std::unique_lock<std::mutex> lk(get_lock()); //<--2
  do_something();
}

Locking at an appropriate granularity

在mutex里面不要做IO操作,他们比读取相同数据量的内存操作要慢成百上千倍

void get_and_process_data()
{
  std::unique_lock<std::mutex> my_lock(the_mutex);
  some_class data_to_process=get_next_data_chunk();
  my_lock.unlock(); //<-- 1 Don’t need mutex locked across call to process()
  result_type result=process(data_to_process);
  my_lock.lock();   //<-- 2 Relock mutex to write result
  write_result(data_to_process,result);
}

在获取锁之后,不要去执行额外不需要锁参与的任务,另外也不要去等待IO操作等。

class Y
{
private:
int some_detail;
  mutable std::mutex m;
  int get_detail() const
  {
    std::lock_guard<std::mutex> lock_a(m); //<--1
    return some_detail;
  }
public:
  Y(int sd):some_detail(sd){}
  friend bool operator==(Y const& lhs, Y const& rhs)
  {
    if(&lhs==&rhs)
      return true;
    int const lhs_value=lhs.get_detail();  //<--2
    int const rhs_value=rhs.get_detail();  //<--3
    return lhs_value==rhs_value;           //<--4
  }
};
  1. 上面的代码有明显的问题,粒度太细.所以在两次get_detail之间隙.lhs和rhs中的内容都可能遭到其他线程修改.
  2. 粒度太粗也可能多线程完全沦为单线程.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值