RAII 线程安全获取器

Posted

技术标签:

【中文标题】RAII 线程安全获取器【英文标题】:RAII thread safe getter 【发布时间】:2015-11-12 15:21:48 【问题描述】:

大多数时候我在代码中看到线程安全 getter 方法的这种实现的一些变体:

class A

public:

    inline Resource getResource() const
    
        Lock lock(m_mutex);

        return m_resource;
    

private:
    Resource m_resource;
    Mutex    m_mutex;
;

假设类 Resource 不能被复制,或者复制操作的计算成本太高,C++ 中有没有办法避免返回副本但仍然使用 RAII 风格的锁定机制?

【问题讨论】:

您使用的是哪个 C++ 版本? “有没有办法避免退回副本”是的,根本不退回。 返回const Resource&? @cad 那不是线程安全的 @cad 返回引用的问题是,在通过 RAII 调用 getResource 后锁被破坏,现在获得该引用的任何人都可以访问 m_resource 而不会锁定资源。 【参考方案1】:

我还没有尝试过,但是这样的东西应该可以工作:

#include <iostream>
#include <mutex>
using namespace std;

typedef std::mutex Mutex;
typedef std::unique_lock<Mutex> Lock;

struct Resource 
    void doSomething() printf("Resource::doSomething()\n"); 
;

template<typename MutexType, typename ResourceType>
class LockedResource

public:
    LockedResource(MutexType& mutex, ResourceType& resource) : m_mutexLocker(mutex), m_pResource(&resource) 
    LockedResource(MutexType& mutex, ResourceType* resource) : m_mutexLocker(mutex), m_pResource(resource) 
    LockedResource(LockedResource&&) = default;
    LockedResource(const LockedResource&) = delete;
    LockedResource& operator=(const LockedResource&) = delete;

    ResourceType* operator->()
    
        return m_pResource;
    

private:
    Lock m_mutexLocker;
    ResourceType* m_pResource;
;

class A

public:

    inline LockedResource<Mutex, Resource> getResource()
    
        return LockedResource<Mutex, Resource>(m_mutex, &m_resource);
    

private:
    Resource m_resource;
    Mutex    m_mutex;
;


int main()

    A a;
     //Lock scope for multiple calls
        auto r = a.getResource();
        r->doSomething();
        r->doSomething();

        // The next line will block forever as the lock is still in use
        //auto dead = a.getResource();

     // r will be destroyed here and unlock
    a.getResource()->doSomething();
    return 0;

但要小心,因为被访问资源的生命周期取决于所有者的生命周期 (A)


Godbolt 示例:Link

P1144 很好地减少了生成的程序集,因此您可以看到锁在哪里锁定和解锁。

【讨论】:

无法理解 main() 中嵌套作用域的原因 @nyarlathotep108 该范围是为了指示锁的开始和结束位置,但我没有存储 LockedRessource 的实例,因此搞砸了。忽略它。我目前正在使用移动设备,但明天我将删除该范围。 @nyarlathotep108 我希望现在更有意义。 原来我过早地赞成这个。不幸的是它没有编译。第一个问题是LockedResources 移动构造函数,这里使用std::lock_guard 无效,不可移动。用std::unique_lock 替换它可以解决这个问题。下一个问题是A::getResource() 的返回类型,它本质上导致返回对本地临时的引用。将其替换为 LockedResource&lt;Mutex, Resource&gt; 即可解决此问题。 .您能否编辑您的答案以使其有效?谢谢! @ahans 就像我提到的,我没有测试过代码,它更像是某种模板。感谢您指出缺陷 - 我现在修复了提到的问题。【参考方案2】:

返回一个为Resource 类提供线程安全接口和/或保留一些锁的访问器对象怎么样?

class ResourceGuard 
private:
    Resource *resource;

public:
    void thread_safe_method() 
        resource->lock_and_do_stuff();
    

这将以 RAII 方式清除,并在需要时释放任何锁。如果您需要锁定,则应在 Resource 类中完成。

当然你要注意Resource的寿命。一个非常简单的方法是使用std::shard_ptr。一个weak_ptr 也可能适合。

【讨论】:

打败我。就个人而言,我会存储对m_resource 的引用而不是指针,并通过std::unique_ptr&lt;ResourceGuard&gt; 返回守卫。 std::shard_ptr 可以复制到其他线程,并且拥有相同互斥锁的许多线程将是一场灾难。 您能否将答案中的代码与吸气剂代码整合在一起?如果我每次都返回一个 ResourceGuard 对象并且它内部有自己的互斥锁,我看不出这是如何工作的...... 你当然是对的。访问接口时要加锁。【参考方案3】:

实现相同目的的另一种方法。这是可变版本的实现。 const 访问器同样简单。

#include <iostream>
#include <mutex>

struct Resource


;

struct locked_resource_view

    locked_resource_view(std::unique_lock<std::mutex> lck, Resource& r)
    : _lock(std::move(lck))
    , _resource(r)
    

    void unlock() 
        _lock.unlock();
    

    Resource& get() 
        return _resource;
    

private:
    std::unique_lock<std::mutex> _lock;
    Resource& _resource;
;

class A

public:

    inline locked_resource_view getResource()
    
        return 
            std::unique_lock<std::mutex>(m_mutex),
            m_resource
        ;
    

private:
    Resource m_resource;
    mutable std::mutex    m_mutex;
;

using namespace std;

auto main() -> int

    A a;
    auto r = a.getResource();
    // do something with r.get()

    return 0;

【讨论】:

现在这更接近我即将发布的内容(比 ssteinberg 的原始答案)。我会这样定义构造函数:locked_resource_view(std::mutex&amp; m, Resource&amp; r)。这样就可以就地构建锁。 当然这可能更整洁。无论如何,多余的移动很可能会被优化掉。

以上是关于RAII 线程安全获取器的主要内容,如果未能解决你的问题,请参考以下文章

使函数线程安全的类

多线程的互斥锁应用RAII机制

多线程的互斥锁应用RAII机制

每个线程的 Java 安全管理器

并行流是不是以线程安全的方式处理上游迭代器?

Java -- 每日一问:如何保证集合是线程安全的? ConcurrentHashMap如何实现高效地线程安全?