【问题标题】:How can I create a smart pointer that locks and unlocks a mutex?如何创建锁定和解锁互斥锁的智能指针?
【发布时间】:2013-03-30 08:54:25
【问题描述】:

我有一个线程类,我想偶尔从中获取一个实例变量的指针。我希望这种访问受到互斥锁的保护,以便在客户端完成其指针之前阻止线程访问此资源。

我最初的方法是返回一对对象:一个是指向资源的指针,一个是指向互斥锁对象的 shared_ptr。这个 shared_ptr 持有对锁定对象的唯一引用,因此当它超出范围时应该解锁互斥锁。像这样的:

void A::getResource()
{
    Lock* lock = new Lock(&mMutex);
    return pair<Resource*, shared_ptr<Lock> >(
        &mResource, 
        shared_ptr<Lock>(lock));
}

此解决方案不太理想,因为它需要客户端持有整对对象。这样的行为破坏了线程安全:

Resource* r = a.getResource().first;

此外,我自己的实现是死锁,我很难确定原因,所以可能还有其他问题。

我想要的是一个包含锁作为实例变量的 shared_ptr,将它与访问资源的方法绑定。这似乎应该有一个既定的设计模式,但做了一些研究后,我惊讶地发现它很难遇到。

我的问题是:

  • 这种模式有通用的实现方式吗?
  • 将互斥锁放入我忽略的 shared_ptr 中是否存在问题,从而阻止了这种模式的广泛传播?
  • 是否有充分的理由不实现我自己的 shared_ptr 类来实现此模式?

(注意我正在开发一个使用 Qt 的代码库,但不幸的是在这种情况下不能使用 boost。但是,涉及 boost 的答案仍然是普遍感兴趣的。)

【问题讨论】:

  • riv 和 Jonanthan Wakely 的回答都很有趣,值得关注。我选择 Riz 只是因为很高兴在答案中有一些完整的协作编辑代码。

标签: c++ thread-safety mutex smart-pointers raii


【解决方案1】:

我不确定是否有任何标准实现,但由于我喜欢无缘无故地重新实现东西,所以这里有一个版本应该可以工作(假设你不想复制这样的指针):

template<class T>
class locking_ptr
{
public:
  locking_ptr(T* ptr, mutex* lock)
    : m_ptr(ptr)
    , m_mutex(lock)
  {
    m_mutex->lock();
  }
  ~locking_ptr()
  {
    if (m_mutex)
      m_mutex->unlock();
  }
  locking_ptr(locking_ptr<T>&& ptr)
    : m_ptr(ptr.m_ptr)
    , m_mutex(ptr.m_mutex)
  {
    ptr.m_ptr = nullptr;
    ptr.m_mutex = nullptr;
  }

  T* operator ->()
  {
    return m_ptr;
  }
  T const* operator ->() const
  {
    return m_ptr;
  }
private:
  // disallow copy/assignment
  locking_ptr(locking_ptr<T> const& ptr)
  {
  }
  locking_ptr& operator = (locking_ptr<T> const& ptr)
  {
    return *this;
  }
  T* m_ptr;
  mutex* m_mutex; // whatever implementation you use
};

【讨论】:

  • +1 这应该可以 - 也许禁用分配并添加移动构造函数?
  • 当然,您可以通过将operator = 移动到私有部分来禁用分配,但只要互斥锁支持嵌套锁,它就应该是安全的。添加了移动构造函数。
  • 你为什么在你的复制构造函数中分配m_mutex = nullptr?您不会在析构函数中进行任何完整性检查,因此您最终会在空指针上调用 unlock()
  • 即使考虑到可能会出现离题讨论的危险,我也不想引发任何话题,但 “我通常避免使用标准库” - 哎哟!
  • 为什么要实现拷贝构造函数和拷贝赋值运算符?要么将它们设为私有且未实现,要么停止生活在 1998 年并将它们定义为已删除。
【解决方案2】:

您正在描述 EXECUTE AROUND POINTER 模式的变体,由 Kevlin Henney 在 Executing Around Sequences 中描述。

我在exec_around.h 有一个原型实现,但我不能保证它在所有情况下都能正常工作,因为它正在进行中。它包含一个函数mutex_around,它创建一个对象并将其包装在一个智能指针中,该指针在访问时锁定和解锁互斥体。

【讨论】:

    【解决方案3】:

    这里还有另一种方法。灵活性和通用性要低得多,但也简单得多。虽然它似乎仍然适合您的确切情况。

    shared_ptrstandardBoost)提供了构造它的方法,同时提供了另一个shared_ptr 实例,该实例将用于使用计数器和一些根本不会被管理的任意指针。在cppreference.com 上,它是第 8 种形式 (the aliasing constructor)。

    现在,这种形式通常用于转换——比如从派生类对象向基类对象提供shared_ptr。它们共享所有权和使用计数器,但(通常)有两个不同类型的不同指针值。此表单还用于为基于shared_ptr 的成员值提供shared_ptr,以提供它所属的对象。

    在这里我们可以“滥用”表单来提供锁保护。这样做:

    auto A::getResource()
    {
        auto counter = std::make_shared<Lock>(&mMutex);
        std::shared_ptr<Resource> result{ counter, &mResource };
        return result;
    }
    

    返回的shared_ptr 指向mResource 并保持mMutex 被锁定,只要它被任何人使用。

    此解决方案的问题在于,您现在有责任确保mResource 在此期间也保持有效(特别是 - 它不会被破坏)。如果锁定 mMutex 就足够了,那么你就可以了。

    否则,上述解决方案必须根据您的特定需求进行调整。例如,您可能希望拥有一个简单的struct counter,将Lock 和另一个shared_ptr 保留到拥有AA 对象mResource

    【讨论】:

    • 如果使用std::mutexLock应该是什么类型?
    • @Androvich, std::lock_guard 您的回答似乎很好。您也可以使用std::unique_lock 或该概念的任何其他实现。但是,对于目前的需求,std::lock_guard 就足够了,它也是最简单的。
    • 是的,它运行良好。另外,在您的回答中,不应该是std::make_shared&lt;Lock&gt;(mMutex) 而不是std::make_shared&lt;Lock&gt;(&amp;mMutex)
    • @Androvich,我不这么认为。 Lock 将是与 mMutex 不同的类型。
    【解决方案4】:

    要添加到Adam Badura's answer,对于使用std::mutexstd::lock_guard 的更一般的情况,这对我有用:

    auto A::getResource()
    {
        auto counter = std::make_shared<std::lock_guard<std::mutex>>(mMutex);
        std::shared_ptr<Resource> ptr{ counter, &mResource} ;
        return ptr;
    }
    

    std::mutex mMutexResource mResource 的生命周期由某个类 A 管理。

    【讨论】:

      猜你喜欢
      • 2018-05-23
      • 2021-12-23
      • 1970-01-01
      • 2021-11-23
      • 2012-12-25
      • 2022-07-31
      • 2021-02-03
      • 2010-12-17
      • 1970-01-01
      相关资源
      最近更新 更多