未拥有的互斥锁的解锁

4

我创建了以下类,提供了acquire_lock()release_lock()函数

class LockableObject {

public:

    void acquire_lock() {
        std::unique_lock<std::mutex> local_lock(m_mutex);
        m_lock = std::move(local_lock);
    }

    void release_lock() {
        m_lock.unlock();
    }

private:

    std::mutex m_mutex;

    std::unique_lock<std::mutex> m_lock;
};

这个类提供了acquire_lockrelease_lock函数。我有多个线程访问同一个对象,并在执行任何操作之前调用acquire_lock,然后在完成后调用release_lock

void ThreadFunc(int ID, LockableObject* lckbleObj)
{
    for (int i = 0; i < 1000; i++)
    {
        lckbleObj->acquire_lock();
        std::cout << "Thread with ID = " << ID << "doing work" << std::endl;
        std::this_thread::sleep_for(std::chrono::milliseconds(10));
        lckbleObj->release_lock();
    }
}



void main()
{
    const int numThreads = 10;
    std::thread workerThreads[numThreads];
    LockableObject *testObject = new LockableObject();

    for (int i = 0; i < numThreads; i++)
    {
        workerThreads[i] = std::thread(ThreadFunc, i, testObject);
    }

    for (int i = 0; i < numThreads; i++)
    {
        workerThreads[i].join();
    }
}

acquire_lock函数中,我首先使用本地堆栈std::unique_lock对象通过在构造函数中传递(m_mutex)来尝试锁定底层互斥量(m_mutex)。 我假设一旦std::unique_lock的构造函数返回它已经锁定了互斥量,然后将堆栈上的unique_lock移动到成员变量m_lock上。
这个程序存在某些基本问题,在调用release_lock时会导致"unlock of unowned mutex",我似乎缺少关于std::unique_lock的某些基本理解,希望有人能纠正我的理解。

2
std::unique_lock的构造函数中,您没有传递std::defer_lock,因此在构造它时已经锁定。然后,您尝试对其进行加锁,这应该会抛出一个std::system_error。您确定这是正确的代码吗? - David Schwartz
m_lock = std::unique_lockstd::mutex(m_mutex); 而不是 std::move。 - leiyc
@DavidSchwartz 你说得对,我把一些代码错误地复制到了问题中。我已经通过在构造后删除对锁的调用来纠正了代码,现在只有在解锁时才会失败。或者,我还尝试在构造local_lock时将std::defer_lock作为第二个参数调用构造函数,然后显式调用local_lock的lock()方法,但行为仍然相同,它运行几次迭代,然后出现“未拥有互斥锁的解锁”错误。 - Gr-Disarray
@arunsun,请看下面我的回答。你还有一个竞态条件。 - David Schwartz
2个回答

6

请参考我有关构造函数中缺乏std::defer_lock的评论。但您的代码还存在竞争条件。

acquire_lock函数在m_mutex互斥锁的保护下修改m_lock。因此,为确保线程安全,除了持有m_mutex的线程以外,其他线程不能修改m_lock

但是,在释放互斥锁时,release_lock函数会修改m_lock。因此,您的m_lock同步不正确。

这可能有些微妙难懂。以下是问题代码:

    m_lock.unlock();

注意,在进入此函数时,m_mutex被锁定,但在执行期间,它会以无特定保证的顺序修改m_lock并释放m_mutex。但是m_mutex保护了m_lock。因此,这是一种竞态条件,不允许发生。
可以按照以下方式修复:
void release_lock() {
    std::unique_lock<std::mutex> local_lock = std::move(m_lock);
    local_lock.unlock();
}

现在,这行代码的第一句修改了m_lock,但整个过程都在持有m_mutex的情况下运行,以避免竞争条件。

如果需要,可以移除unlock。因为local_lock的析构函数会自动执行它。

顺便说一句,我建议改变API。不要提供锁定和解锁调用,而是提供一种创建拥有该对象锁定的对象的方法。如果你想的话,甚至可以使用std::unique_lock<LockableObject>。为什么要创建一个比标准库提供的更糟糕的新API呢?


我相信那只是一个打字错误。 - Gr-Disarray
@David 真是太感谢你的帮助了,看起来确实是竞态条件,我还在努力理解这里的具体细节,但对这两个操作的影响感到惊讶。 - Gr-Disarray
我的理解对这个竞争是否准确 - 查看std_mutex.h中unlock的实现,我发现该函数首先在_M_device上调用unlock,然后在此之后将_M_owns设置为false,假设现在有一个线程是解锁代码,并恰好调用_M_device上的unlock,但尚未将_M_owns设置为false,在此期间另一个线程锁定,然后在第二个线程调用unlock之前,第一个线程现在将_M_owns设置为false,导致第二个线程出错。 - Gr-Disarray
我也花了一些时间考虑您更改API的建议,您可能对API不太吸引人有道理,但是当我想出它时,我有以下思考过程:我有一组由多个线程处理的共享对象。在我的头脑中,我发现将锁信息嵌入这些被锁定的对象中很方便,以避免与被锁定对象关联的中央锁对象的额外查找。有什么更好的建议吗? - Gr-Disarray
@arunsun 我认为这不是一个很有帮助的思考方式。正确的思考方式是,如果没有同步,你不能在多个线程中访问对象。按照你的方式思考会导致错误的想法,即当你认为没有任何可能失败的方式时,就意味着它不会失败。在这种情况下,你可以想到一种可能失败的方式,但即使你想不到,也并不意味着它不会失败。 - David Schwartz

1

可以像下面这样更改成员函数acquire_lock来解决问题:

void acquire_lock() {
    m_lock = std::unique_lock<std::mutex>(m_mutex);
}

m_lock中,将调用unique_lockmove-assign function来管理互斥对象m_mutex


我相信另一个答案是准确的,问题似乎确实发生在release_lock()函数中,但我很感激你对acquire_lock函数的更新,它似乎与我的acquire_lock等效,但只有一行而不是两行。 - Gr-Disarray

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