这段代码不应该导致死锁吗?

4
我有一个类,其中包含一个互斥锁和一个对象。每次需要访问该包含的对象时,都会调用一个方法来锁定互斥锁并返回包含的对象。下面是代码:


```html 我有一个类,其中包含一个互斥锁和一个对象。每次需要访问该包含的对象时,都会调用一个方法来锁定互斥锁并返回包含的对象。下面是代码: ```
template <typename MUTEX, typename RESOURCE>
class LockedResource
{
    using mutex_t    = MUTEX;
    using resource_t = RESOURCE;

    mutex_t    m_mutex;
    resource_t m_resource;

public:
        template <typename ... ARGS>
        LockedResource(ARGS &&... args) :
                m_resource(std::forward<ARGS>(args) ...)
        {}

    class Handler
    {
        std::unique_lock<mutex_t> m_lock;      // unique lock
        resource_t                &m_resource; // Ref to resource

        friend class LockedResource;

        Handler(mutex_t &a_mutex, resource_t &a_resource) :
            m_lock(a_mutex),       // mutex automatically locked
            m_resource(a_resource)
        { std::cout << "Resource locked\n"; }

    public:
        Handler(Handler &&a_handler) :
            m_lock(std::move(a_handler.m_lock)),
            m_resource(a_handler.m_resource)
        { std::cout << "Moved\n"; }

        ~Handler() // mutex automatically unlocked
        { std::cout << "Resource unlocked\n"; }

        RESOURCE *operator->()
        { return &m_resource; }
    };

    Handler get()
    { return {m_mutex, m_resource}; }
};

template <typename T> using Resource = LockedResource<std::mutex, T>;

这段代码的思路是"包装"一个对象并保护它免受来自多个线程的多次访问;被包装的对象具有私有可见性,唯一访问它的方式是通过内部类"Handler",预期的用法如下:
LockedResource<std::mutex, Foo> locked_foo;
void f()
{
    auto handler = locked_foo.get(); // this will lock the locked_foo.m_mutex;
    handler->some_foo_method();
    // going out of the scope will call the handler dtor and
    // unlock the locked_foo.m_mutex;
}

所以,如果我没有错的话,调用LockedResource::get方法会创建一个LockedResource::Handle值,该值会在整个Handle的生命周期中锁定LockedResource::m_mutex...但我一定是错误的,因为下面的代码不会导致死锁:

LockedResource<std::mutex, std::vector<int>> locked_vector{10, 10};

int main()
{
/*1*/  auto vec = locked_vector.get(); // vec = Resource<vector>::Handler
/*2*/  std::cout << locked_vector.get()->size() << '\n';
/*3*/  std::cout << vec->size() << '\n';
    return 0;
}

我原本期望代码中的/*1*/行会锁定locked_vector.m_mutex,而/*2*/行会尝试锁定已经被锁定的同一互斥量导致死锁,但实际输出结果如下:

Resource locked
Resource locked
10
Resource unlocked
10
Resource unlocked
  • 第二个::get()不应该导致死锁吗?
  • 我是通过同一个锁来访问包装的资源还是我理解错了什么?

这里是示例代码


1
如果互斥锁的实现符合 C++11 标准,那么你可能会遇到死锁(或者抛出异常,或者触发 assert)。然而,如果 std::mutex 是一个递归互斥锁的包装器(这在嵌入式系统中很常见),那么你将看到上述输出。 - Mehrwolf
1
使用GCC 5.1时,行为是不同的。例如,请参见此处 - Mehrwolf
2个回答

7

快速测试结果如下:

  1. GCC - 显示与问题中相同的输出。
  2. Clang - 在我使用的在线编译器上被终止。所以是死锁。
  3. MSVC2013 - 抛出“device or resource busy:device or resource busy”错误。它检测到在同一线程上试图锁定已经锁定的互斥对象。

标准对此有何规定?

30.4.1.2.1/4 [注意:如果拥有互斥对象的线程在该对象上调用lock(),程序可能会发生死锁。如果实现程序可以检测到死锁,则可能观察到resource_deadlock_would_occur错误条件。—注]

但根据30.4.1.2/13,应抛出以下其中之一:

— resource_deadlock_would_occur — if the implementation detects that a deadlock would occur. 
— device_or_resource_busy — if the mutex is already locked and blocking is not possible.

所以答案是肯定的,你观察到的是不正确的行为。它应该阻止或抛出异常,而不是继续执行下去。
由于代码中存在未定义行为,因此可能会观察到此行为。根据17.6.4.11,违反Requires条款是未定义行为,在30.4.1.2/7中我们有以下要求: Requires: 如果m是std::mutex、std::timed_mutex或std::shared_timed_mutex类型,则调用线程不拥有互斥量。
感谢@T.C.指出UB。

在你提到之前,我没有使用其他编译器测试过这段代码,但是使用Wandbox代替Ideone后,我得到了与你相同的结果...我没想到锁定行为会因编译器和版本而异 :O - PaperBirdMaster
1
观察到的行为是完全正确的。锁定已经拥有的互斥量违反了[thread.mutex.requirements.mutex]/p7,并且违反Requires条款是UB。此外,注释不是规范性的。 - T.C.

0

我不熟悉这个具体的互斥/资源实现,但是这种同步原语通常包含一个锁定计数,并允许同一线程多次锁定同一对象。

当互斥已经解锁的次数与它被锁定的次数相同时,另一个线程就可以自由地锁定它。


2
那应该是一个递归互斥锁。OP 没有使用它。 - ixSci
就我个人而言,我认为对您回答进行的负评是错误的。 - Ami Tavory

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接