试图创建线程安全的std :: map

时间:2014-10-15 20:28:22

标签: c++ multithreading c++11 mutex

假设我们有std::map容器,我们希望在插入,擦除,搜索和编辑记录方面使其成为线程安全的。同时我们希望线程可以并行处理不同的记录(读取和编辑记录)。为此,我为记录编辑操作创建了一个单独的类,它使用互斥锁保护。

class Data
{
public:
    Data(const std::string& data) : _mutex(), _data(data) { }
    void setData(const std::string& data)
    {
        std::lock_guard<std::mutex> locker(_mutex);
        _data = data;
    }

    const std::string& getData() const { return _data; }

private:
    std::mutex _mutex;
    std::string _data;
};

class Storage
{
public:
    void insertData(size_t key, const std::string& data)
    {
        std::lock_guard<std::mutex> locker(_mutex);
        _storage[key] = data;
    }

    void eraseData(size_t key)
    {
        std::lock_guard<std::mutex> locker(_mutex);
        _storage.erase(key);
    }

    const std::string& getData(size_t key) const { return _storage[key].getData(); }

    void setData(size_t key, const std::string& data) { _storage[key].setData(data); }

private:
    std::mutex _mutex;
    std::map<size_t, Data> _storage;
};

现在假设线程抓住&#34;本地&#34;要编辑的某些记录的互斥锁(Data::setData方法调用)。在同一时间,其他线程抓住&#34;全球&#34; mutex删除此记录(Storage::eraseData方法调用) - 有什么问题吗?此代码还有哪些其他问题?

2 个答案:

答案 0 :(得分:3)

首先解决您的并发问题。这是一个C ++ 14解决方案,因为C ++ 11版本更冗长,而且我们没有我们想要的所有锁定原语:

template<class T>
struct thread_safe {
  template<class F>
  auto read( F&& f ) const {
    std::shared_lock<decltype(mutex)> lock(mutex);
    return std::forward<F>(f)(t);
  }
  template<class F>
  auto write( F&& f ) {
    std::unique_lock<decltype(mutex)> lock(mutex);
    return std::forward<F>(f)(t);
  }
  template<class O>
  thread_safe(O&&o):t(std::forward<O>(o)) {}

  thread_safe() = default;

  operator T() const {
    return o.read([](T const& t){return t;});
  }

  // it is really this simple:
  thread_safe( thread_safe const& o ):t( o ) {}

  // forward to above thread safe copy ctor:
  thread_safe( thread_safe & o ):thread_safe( const_cast<thread_safe const&>(o) ) {}
  thread_safe( thread_safe && o ):thread_safe(o) {}
  thread_safe( thread_safe const&& o ):thread_safe(o) {}

  thead_safe& operator=( thread_safe const& o ) {
    write( [&o](auto& target) {
      target = o;
    });
    return *this;
  } 
  template<class O>
  thread_safe& operator=( O&& o ) {
    write([&o](auto& t){ t = std::forward<O>(o); });
    return *this;
  }
private:
  T t;
  mutable std::shared_timed_mutex mutex;
};

这是围绕任意类的线程安全包装。

我们可以直接使用它:

typedef thread_safe< std::map< size_t, thread_safe<std::string> > > my_map;

这里我们有两级线程安全地图。

使用示例,将条目33设置为"hello"

my_map.write( [&](auto&& m){
  m[33] = "hello";
} );

每个元素和地图上都有许多读者,单作者。从readwrite电话中返回迭代器并不安全。

当然,您应该测试并审核上述代码。我没有。

核心理念非常简单。要阅读,您必须.read线程安全对象。传入的lambda获取基础数据const&。在std::数据上,保证这些数据是多读者安全的。

要写,您必须.write。这会获得一个独占锁定,阻止其他.read。这里的lambda得到基础数据的&

我添加了operator T=以及copy-construct以使类型更加规则。这样做的代价是你可能会意外地产生很多锁定/解锁行为。优点是m[33] = "hello" 正常工作,这很棒。

答案 1 :(得分:1)

你有两个大问题:

  1. 如果一个线程在另一个线程调用insertData的同时调用getData会发生什么?对operator[]的调用可能会崩溃,因为地图在尝试访问时会被修改。

  2. 如果一个线程调用eraseData而另一个线程仍在使用从getData返回的引用,会发生什么?引用可能无效,导致崩溃。