03-多线程共享数据竞争——初始化和读写锁

2020/07/03 cpp_concurrency 共 2458 字,约 8 分钟

如果某一数据需要初始化之后再进行多线程共享,或者某一资源大多数时候都是只读操作,那么此时如果使用互斥锁将会造成比较大的性能浪费。在传统的系统编程中可以使用读写锁来完成,但是C++11标准并没有将读写锁纳入标准库。虽然没有提供读写锁,但是我们可以很容易的实现一个自己的读写锁,同时C++11标准库也提供了数据初始化操作——std::once_flag和std::once_call——它们保证相关的操作只在一个线程上完成。

初始化竞争

std::once_flag 和 std::call_once

C++标准库提供了 std::once_flag 和 std::call_once 条件竞争,它一般应用于条件初始化中。比起锁住互斥量,并显式的检查指针,每个线程只需要使用std::call_once,在std::call_once的结束时,它保证相关资源已经被安全的初始化完成了。使用std::call_once比显式使用互斥量消耗的资源更少,特别是当初始化完成后。

call_once调用后为啥资源就是已经线程安全的初始化了呢??实际上就是cpp保证了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();
}

实际上,要实现一个我们自己的call_once也是很简单的,只需要提供一个std::atomit的原子变量作为是否初始化的标志进行判断即可,只是既然标准库提供了once_flag和call_once,那么就不要多此一举了。

static初始化竞争

还有一种情形的初始化过程中潜存着条件竞争:其中一个局部变量被声明为static类型。这种变量的在声明后就已经完成初始化;对于多线程调用的函数,这就意味着这里有条件竞争——抢着去定义这个变量。在很多C++11之前的编译器(译者:不支持C++11标准的编译器),在实践过程中,这样的条件竞争是确实存在的,因为在多线程中,每个线程都认为他们是第一个初始化这个变量的线程;或一个线程对变量进行初始化,而另外一个线程要使用这个变量时,初始化过程还没完成。在C++11标准中,这些问题都被解决了:初始化及定义完全在一个线程中发生,并且没有其他线程可在初始化完成前对其进行处理,条件竞争终止于初始化阶段,这样比在之后再去处理好的多。在只需要一个全局实例情况下,这里提供一个std::call_once的替代方案

class my_class;
my_class& get_my_class_instance()
{
  static my_class instance;  // 在C++11中是线程安全的初始化过程
  return instance;
}

读写锁

之所以要有读写锁,是因为有些情况下被共享的资源很少被修改,这样如果还是使用mutex进行同步,那么如果只是进行读取操作的线程仍然需要排队加锁,这样浪费大量的CPU时间。

读写锁的作用

  • 拥有写锁的时候,其他线程不能读写
  • 拥有读锁的时候,其他线程可以读,但是不能写

cpp11没有引入读写锁,但是boost库提供了共享锁——boost::shared_mutex。当任一线程拥有一个共享锁时,这个线程就会尝试获取一个独占锁,直到其他线程放弃他们的锁;同样的,当任一线程拥有一个独占锁时,其他线程就无法获得共享锁或独占锁,直到第一个线程放弃其拥有的锁。

#include <map>
#include <string>
#include <mutex>
#include <boost/thread/shared_mutex.hpp>

class dns_entry;

class dns_cache
{
  std::map<std::string,dns_entry> entries;
  mutable boost::shared_mutex entry_mutex;
public:
  dns_entry find_entry(std::string const& domain) const
  {
    boost::shared_lock<boost::shared_mutex> lk(entry_mutex);  // 1,获取共享锁,其他程序仍然可以调用find_entry并且不会阻塞,但是如果调用update_or_add_entry则会阻塞
    std::map<std::string,dns_entry>::const_iterator const it=
       entries.find(domain);
    return (it==entries.end())?dns_entry():it->second;
  }
  void update_or_add_entry(std::string const& domain,
                           dns_entry const& dns_details)
  {
    std::lock_guard<boost::shared_mutex> lk(entry_mutex);  // 2,获取独占锁,其他程序不管调用find_entry还是update_or_add_entry都会阻塞直到更新完毕
    entries[domain]=dns_details;
  }
};

文档信息

Search

    Table of Contents