标准C ++中的共享递归互斥体

时间:2016-04-14 10:00:08

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

为C ++ 17计划了一个shared_mutex课程。并且shared_timed_mutex已经在C ++ 14中了。 (谁知道他们为什么按顺序排列,但无论如何。)然后从C ++ 11开始有一个recursive_mutex和一个recursive_timed_mutex。我需要的是shared_recursive_mutex。我是否错过了标准中的某些内容,还是需要再等三年才能获得标准化版本?

如果目前没有这样的设施,那么仅使用标准C ++的这种功能的简单(第一优先级)和有效(第二优先级)实现是什么?

5 个答案:

答案 0 :(得分:7)

互斥锁的

递归属性与术语所有者一起运行,如果 shared_mutex 没有明确定义:多个线程可能{ {1}}同时打电话。

假设所有者作为调用.lock_shared()(而非.lock()!)的线程,递归共享互斥锁的实现可以简单地从.lock_shared()派生:

shared_mutex

字段class shared_recursive_mutex: public shared_mutex { public: void lock(void) { std::thread::id this_id = std::this_thread::get_id(); if(owner == this_id) { // recursive locking count++; } else { // normal locking shared_mutex::lock(); owner = this_id; count = 1; } } void unlock(void) { if(count > 1) { // recursive unlocking count--; } else { // normal unlocking owner = std::thread::id(); count = 0; shared_mutex::unlock(); } } private: std::atomic<std::thread::id> owner; int count; }; 需要声明为原子,因为在.owner方法中,它会在没有并发访问保护的情况下进行检查。

如果您想以递归方式调用.lock()方法,则需要维护所有者地图,并且应使用其他互斥锁保护对该地图的访问。

允许活动.lock_shared()的帖子调用.lock()会使实施变得更加复杂。

最后,允许线程提前.lock_shared()锁定到.lock_shared() 禁止,因为它会导致两个线程可能出现死锁试图进行推进。

同样,递归 共享互斥锁的语义会非常脆弱,所以最好不要使用它。

答案 1 :(得分:4)

如果您使用的是Linux / POSIX平台,那么您很幸运,因为C ++互斥体是在POSIX之后建模的。 POSIX提供了更多功能,包括递归,进程共享等。将POSIX原语包装到C ++类中是很简单的。

Good entry point into POSIX threads documentation

答案 2 :(得分:3)

这是围绕类型T的快速线程安全包装器:

template<class T, class Lock>
struct lock_guarded {
  Lock l;
  T* t;
  T* operator->()&&{ return t; }
  template<class Arg>
  auto operator[](Arg&&arg)&&
  -> decltype(std::declval<T&>()[std::declval<Arg>()])
  {
    return (*t)[std::forward<Arg>(arg)];
  }
  T& operator*()&&{ return *t; }
};
constexpr struct emplace_t {} emplace {};
template<class T>
struct mutex_guarded {
  lock_guarded<T, std::unique_lock<std::mutex>>
  get_locked() {
    return {{m},&t};
  }
  lock_guarded<T const, std::unique_lock<std::mutex>>
  get_locked() const {
    return {{m},&t};
  }
  lock_guarded<T, std::unique_lock<std::mutex>>
  operator->() {
    return get_locked();
  }
  lock_guarded<T const, std::unique_lock<std::mutex>>
  operator->() const {
    return get_locked();
  }
  template<class F>
  std::result_of_t<F(T&)>
  operator->*(F&& f) {
    return std::forward<F>(f)(*get_locked());
  }
  template<class F>
  std::result_of_t<F(T const&)>
  operator->*(F&& f) const {
    return std::forward<F>(f)(*get_locked());
  }
  template<class...Args>
  mutex_guarded(emplace_t, Args&&...args):
    t(std::forward<Args>(args)...)
  {}
  mutex_guarded(mutex_guarded&& o):
    t( std::move(*o.get_locked()) )
  {}
  mutex_guarded(mutex_guarded const& o):
    t( *o.get_locked() )
  {}
  mutex_guarded() = default;
  ~mutex_guarded() = default;
  mutex_guarded& operator=(mutex_guarded&& o)
  {
    T tmp = std::move(o.get_locked());
    *get_locked() = std::move(tmp);
    return *this;
  }
  mutex_guarded& operator=(mutex_guarded const& o):
  {
    T tmp = o.get_locked();
    *get_locked() = std::move(tmp);
    return *this;
  }

private:
  std::mutex m;
  T t;
};

您可以使用:

mutex_guarded<std::vector<int>> guarded;
auto s0 = guarded->size();
auto s1 = guarded->*[](auto&&e){return e.size();};

两者都做了大致相同的事情,并且仅在互斥锁被锁定时才访问被保护对象。

从@tsyvarev的回答中窃取(稍作修改)我们得到:

class shared_recursive_mutex
{
  std::shared_mutex m
public:
  void lock(void) {
    std::thread::id this_id = std::this_thread::get_id();
    if(owner == this_id) {
      // recursive locking
      ++count;
    } else {
      // normal locking
      m.lock();
      owner = this_id;
      count = 1;
    }
  }
  void unlock(void) {
    if(count > 1) {
      // recursive unlocking
      count--;
    } else {
      // normal unlocking
      owner = std::thread::id();
      count = 0;
      m.unlock();
    }
  }
  void lock_shared() {
    std::thread::id this_id = std::this_thread::get_id();
    if (shared_counts->count(this_id)) {
      ++(shared_count.get_locked()[this_id]);
    } else {
      m.lock_shared();
      shared_count.get_locked()[this_id] = 1;
    }
  }
  void unlock_shared() {
    std::thread::id this_id = std::this_thread::get_id();
    auto it = shared_count->find(this_id);
    if (it->second > 1) {
      --(it->second);
    } else {
      shared_count->erase(it);
      m.unlock_shared();
    }
  }
private:
  std::atomic<std::thread::id> owner;
  std::atomic<std::size_t> count;
  mutex_guarded<std::map<std::thread::id, std::size_t>> shared_counts;
};

try_locktry_lock_shared作为练习。

锁定和解锁两者共享锁定互斥锁两次(这是安全的,因为分支实际上是关于&#34;这个线程是否控制互斥锁&#34;,而另一个线程无法改变来自&#34的答案;没有&#34;到&#34;是&#34;反之亦然)。您可以使用->*而不是->进行一次锁定,这样可以加快速度(以逻辑中的某些复杂性为代价)。

以上不支持拥有独占锁,然后是共享锁。这很棘手。它不支持拥有共享锁,然后升级到一个唯一的锁,因为当2个线程尝试时,基本上不可能阻止它死锁。

最后一个问题可能就是为什么递归共享互斥锁是一个坏主意。

答案 3 :(得分:2)

可以使用现有基元构造共享递归互斥锁。不过,我不建议这样做。

这并不简单,并且包装现有的POSIX实现(或者您的平台的任何原生实现)很可能更有效。

如果您决定编写自己的实现,那么使其高效仍然取决于特定于平台的详细信息,因此您要么为每个平台编写具有不同实现的接口,要么您正在选择一个平台,而且可以轻松地使用原生(POSIX或其他)设施。

我当然不打算提供示例递归读/写锁实现,因为它对于Stack Overflow答案来说是一个完全不合理的工作量。

答案 4 :(得分:0)

分享我的实施,没有承诺

recursive_shared_mutex.h

#ifndef _RECURSIVE_SHARED_MUTEX_H
#define _RECURSIVE_SHARED_MUTEX_H

#include <thread>
#include <mutex>
#include <map>

struct recursive_shared_mutex
{
public:

    recursive_shared_mutex() :
        m_mtx{}, m_exclusive_thread_id{}, m_exclusive_count{ 0 }, m_shared_locks{}
    {}

    void lock();
    bool try_lock();
    void unlock();

    void lock_shared();
    bool try_lock_shared();
    void unlock_shared();

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

private:

    inline bool is_exclusive_locked()
    {
        return m_exclusive_count > 0;
    }

    inline bool is_shared_locked()
    {
        return m_shared_locks.size() > 0;
    }

    inline bool can_exclusively_lock()
    {
        return can_start_exclusive_lock() || can_increment_exclusive_lock();
    }

    inline bool can_start_exclusive_lock()
    {
        return !is_exclusive_locked() && (!is_shared_locked() || is_shared_locked_only_on_this_thread());
    }

    inline bool can_increment_exclusive_lock()
    {
        return is_exclusive_locked_on_this_thread();
    }

    inline bool can_lock_shared()
    {
        return !is_exclusive_locked() || is_exclusive_locked_on_this_thread();
    }

    inline bool is_shared_locked_only_on_this_thread()
    {
        return is_shared_locked_only_on_thread(std::this_thread::get_id());
    }

    inline bool is_shared_locked_only_on_thread(std::thread::id id)
    {
        return m_shared_locks.size() == 1 && m_shared_locks.find(id) != m_shared_locks.end();
    }

    inline bool is_exclusive_locked_on_this_thread()
    {
        return is_exclusive_locked_on_thread(std::this_thread::get_id());
    }

    inline bool is_exclusive_locked_on_thread(std::thread::id id)
    {
        return m_exclusive_count > 0 && m_exclusive_thread_id == id;
    }

    inline void start_exclusive_lock()
    {
        m_exclusive_thread_id = std::this_thread::get_id();
        m_exclusive_count++;
    }

    inline void increment_exclusive_lock()
    {
        m_exclusive_count++;
    }

    inline void decrement_exclusive_lock()
    {
        if (m_exclusive_count == 0)
        {
            throw std::logic_error("Not exclusively locked, cannot exclusively unlock");
        }
        if (m_exclusive_thread_id == std::this_thread::get_id())
        {
            m_exclusive_count--;
        }
        else
        {
            throw std::logic_error("Calling exclusively unlock from the wrong thread");
        }
    }

    inline void increment_shared_lock()
    {
        increment_shared_lock(std::this_thread::get_id());
    }

    inline void increment_shared_lock(std::thread::id id)
    {
        if (m_shared_locks.find(id) == m_shared_locks.end())
        {
            m_shared_locks[id] = 1;
        }
        else
        {
            m_shared_locks[id] += 1;
        }
    }

    inline void decrement_shared_lock()
    {
        decrement_shared_lock(std::this_thread::get_id());
    }

    inline void decrement_shared_lock(std::thread::id id)
    {
        if (m_shared_locks.size() == 0)
        {
            throw std::logic_error("Not shared locked, cannot shared unlock");
        }
        if (m_shared_locks.find(id) == m_shared_locks.end())
        {
            throw std::logic_error("Calling shared unlock from the wrong thread");
        }
        else
        {
            if (m_shared_locks[id] == 1)
            {
                m_shared_locks.erase(id);
            }
            else
            {
                m_shared_locks[id] -= 1;
            }
        }
    }

    std::mutex m_mtx;
    std::thread::id m_exclusive_thread_id;
    size_t m_exclusive_count;
    std::map<std::thread::id, size_t> m_shared_locks;
    std::condition_variable m_cond_var;
};

#endif

recursive_shared_mutex.cpp

#include "recursive_shared_mutex.h"
#include <condition_variable>

void recursive_shared_mutex::lock()
{
    std::unique_lock sync_lock(m_mtx);
    m_cond_var.wait(sync_lock, [this] { return can_exclusively_lock(); });
    if (is_exclusive_locked_on_this_thread())
    {
        increment_exclusive_lock();
    }
    else
    {
        start_exclusive_lock();
    }
}

bool recursive_shared_mutex::try_lock()
{
    std::unique_lock sync_lock(m_mtx);
    if (can_increment_exclusive_lock())
    {
        increment_exclusive_lock();
        return true;
    }
    if (can_start_exclusive_lock())
    {
        start_exclusive_lock();
        return true;
    }
    return false;
}

void recursive_shared_mutex::unlock()
{
    {
        std::unique_lock sync_lock(m_mtx);
        decrement_exclusive_lock();
    }
    m_cond_var.notify_all();
}

void recursive_shared_mutex::lock_shared()
{
    std::unique_lock sync_lock(m_mtx);
    m_cond_var.wait(sync_lock, [this] { return can_lock_shared(); });
    increment_shared_lock();
}

bool recursive_shared_mutex::try_lock_shared()
{
    std::unique_lock sync_lock(m_mtx);
    if (can_lock_shared())
    {
        increment_shared_lock();
        return true;
    }
    return false;
}

void recursive_shared_mutex::unlock_shared()
{
    {
        std::unique_lock sync_lock(m_mtx);
        decrement_shared_lock();
    }
    m_cond_var.notify_all();
}

如果线程拥有共享锁,那么它也可能会获得排他锁而不会放弃其共享锁。 (这当然不需要其他线程当前拥有共享锁或排他锁)

反之亦然,拥有排他锁的线程可能会获得共享锁。

有趣的是,这些属性还允许锁可以升级/降级。

临时升级锁:

recusrive_shared_mutex mtx;
foo bar;

mtx.lock_shared();
if (bar.read() == x)
{
    mtx.lock();
    bar.write(y);
    mtx.unlock();
}
mtx.unlock_shared();

从排他锁降级为共享锁

recusrive_shared_mutex mtx;
foo bar;

mtx.lock();
bar.write(x);
mtx.lock_shared();
mtx.unlock();
while (bar.read() != y)
{
     // Something
}
mtx.unlock_shared();