• c++并发编程之互斥锁(mutex)的使用方法


    1. 多个线程访问同一资源时,为了保证数据的一致性,最简单的方式就是使用 mutex(互斥锁)。

    引用 cppreference 的介绍:

    1
    The mutex class is a synchronization primitive that can be used to protect shared data from being simultaneously accessed by multiple threads.

     

    方法1:直接操作 mutex,即直接调用 mutex 的 lock / unlock 函数
    此例顺带使用了 boost::thread_group 来创建一组线程。

    复制代码
    #include <iostream>
    #include <boost/thread/mutex.hpp>
    #include <boost/thread/thread.hpp>
    
    boost::mutex mutex;
    int count = 0;
    
    void Counter() {
      mutex.lock();
    
      int i = ++count;
      std::cout << "count == " << i << std::endl;
    
      // 前面代码如有异常,unlock 就调不到了。
      mutex.unlock();
    }
    
    int main() {
      // 创建一组线程。
      boost::thread_group threads;
      for (int i = 0; i < 4; ++i) {
        threads.create_thread(&Counter);
      }
    
      // 等待所有线程结束。
      threads.join_all();
      return 0;
    }
    复制代码

    方法2:使用 lock_guard 自动加锁、解锁。原理是 RAII,和智能指针类似

    复制代码
    #include <iostream>
    #include <boost/thread/lock_guard.hpp>
    #include <boost/thread/mutex.hpp>
    #include <boost/thread/thread.hpp>
    
    boost::mutex mutex;
    int count = 0;
    
    void Counter() {
      // lock_guard 在构造函数里加锁,在析构函数里解锁。
      boost::lock_guard<boost::mutex> lock(mutex);
    
      int i = ++count;
      std::cout << "count == " << i << std::endl;
    }
    
    int main() {
      boost::thread_group threads;
      for (int i = 0; i < 4; ++i) {
        threads.create_thread(&Counter);
      }
    
      threads.join_all();
      return 0;
    }
    复制代码

    方法3:使用 unique_lock 自动加锁、解锁
    unique_lock 与 lock_guard 原理相同,但是提供了更多功能(比如可以结合条件变量使用)。
    注意:mutex::scoped_lock 其实就是 unique_lock<mutex> 的 typedef

    复制代码
    #include <iostream>
    #include <boost/thread/mutex.hpp>
    #include <boost/thread/thread.hpp>
    
    boost::mutex mutex;
    int count = 0;
    
    void Counter() {
      boost::unique_lock<boost::mutex> lock(mutex);
    
      int i = ++count;
      std::cout << "count == " << i << std::endl;
    }
    
    int main() {
      boost::thread_group threads;
      for (int i = 0; i < 4; ++i) {
        threads.create_thread(&Counter);
      }
    
      threads.join_all();
      return 0;
    }
    复制代码

    方法4:为输出流使用单独的 mutex
    这么做是因为 IO 流并不是线程安全的!
    如果不对 IO 进行同步,此例的输出很可能变成:

    count == count == 2count == 41
    count == 3

    因为在下面这条输出语句中:

    std::cout << "count == " << i << std::endl;

    输出 "count == " 和 i 这两个动作不是原子性的(atomic),可能被其他线程打断。

    复制代码
    #include <iostream>
    #include <boost/thread/mutex.hpp>
    #include <boost/thread/thread.hpp>
    #include <boost/thread/lock_guard.hpp>
    
    boost::mutex mutex;
    boost::mutex io_mutex;
    int count = 0;
    
    void Counter() {
      int i;
      {
        boost::unique_lock<boost::mutex> lock(mutex);
        i = ++count;
      }
    
      {
        boost::unique_lock<boost::mutex> lock(io_mutex);
        std::cout << "count == " << i << std::endl;
      }
    }
    
    int main() {
      boost::thread_group threads;
      for (int i = 0; i < 4; ++i) {
        threads.create_thread(&Counter);
      }
    
      threads.join_all();
      return 0;
    }
    复制代码

    2. 保护共享数据的替代设施

    2.1 保护共享数据的初始化过程

    丑陋的代码:

    复制代码
    void undefined_behaviour_with_double_checked_locking()
    {
        if(!resource_ptr) // 1
        {
           std::lock_guard<std::mutex> lk(resource_mutex);
            if(!resource_ptr) // 2
            {
              resource_ptr.reset(new some_resource); // 3
            }
        }
        resource_ptr->do_something(); // 4
    }    
    复制代码

    这个模式为什么声名狼藉呢?因为这里有潜在的条件竞争,因为外部的读取锁①没有与内部的
    写入锁进行同步③。因此就会产生条件竞争,这个条件竞争不仅覆盖指针本身,还会影响到其
    指向的对象;即使一个线程知道另一个线程完成对指针进行写入,它可能没有看到新创建的
    some_resource实例,然后调用do_something()④后,得到不正确的结果。

    C++标准库提供了 std::once_flag 和 std::call_once 来处理这种情况。比起锁住互斥量,并显式的检查指

    针,每个线程只需要使用 std::call_once ,在 std::call_once 的结束时,就能安全的知道指
    针已经被其他的线程初始化了。使用 std::call_once 比显式使用互斥量消耗的资源更少,特
    别是当初始化完成后。

    复制代码
    std::shared_ptr<some_resource> resource_ptr;
    std::once_flag resource_flag; // 1
    void init_resource()
    {
    resource_ptr.reset(new some_resource);
    }
    void foo()
    {
    std::call_once(resource_flag,init_resource); // 可以完整的进行一次初始化
    resource_ptr->do_something();
    }
    复制代码

    2.2 保护很少更新的数据结构

    虽然更新频度很低,但更新也是有可能发生的,并且当这个可缓存被多个线程访问,这个缓
    存就需要适当的保护措施,来对其处于更新状态时进行保护,也为了确保线程读到缓存中的
    有效数据。

    使用一
    个 std::mutex 来保护数据结构,这的确有些反应过度,因为在没有发生修改时,它将削减并
    发读取数据的可能性;这里需要另一种不同的互斥量。这种新的互斥量常被称为“读者-写者
    锁”(reader-writer mutex),因为其允许两中不同的使用方式:一个“作者”线程独占访问和共
    享访问,让多个“读者”线程并发访问。

    新的C++标准库应该不提供这样的互斥量,Boost库提供了boost::shared_mutex。

    3.3 嵌套锁

    C++标准库提供了 std::recursive_mutex 类。其功能与 std::mutex 类似,除了你可以从
    同一线程的单个实例上获取多个锁。在互斥量锁住其他线程前,你必须释放你拥有的所有
    锁,所以当你调用lock()三次时,你也必须调用unlock()三次。正确使
    用 std::lock_guard<std::recursive_mutex> 和 std::unique_lock<std::recursice_mutex> 可以帮
    你处理这些问题。

  • 相关阅读:
    Java--8--新特性--Lambda
    java--Annotation实现
    原生Ajax代码实现
    java--动态代理设计模式,CGLIB实现的动态代理设计模式
    java--分析简单java类与反射的联系
    java--反射的基本概念
    Oracle-4
    初始化springbean
    spring初始化bean对象的三种方式:init-method,afterPropertiesSet和BeanPostProcessor
    在注释中查看其它的类的方式
  • 原文地址:https://www.cnblogs.com/xiaochouk/p/16163427.html
Copyright © 2020-2023  润新知