使用互斥锁器和手动锁定互斥锁的区别

7
具体来说,如果在常量成员函数中,在开头使用mutex.lock(),并在返回前使用mutex.unlock(),则在OpenMP循环内运行时会导致崩溃。但是,如果将这两个调用替换为一个QMutexLocker(&mutex),则可以平稳运行。 Visual Studio 2010,Qt 4.8。 我希望这两个代码等效,但显然它们不是。我在这里错过了什么? 编辑: 虽然这不能复制问题,以下是一个小例子:
class TileCache
{
public:
    bool fillBuffer(const std::string& name) const {
        //QMutexLocker lock(&mCacheMutex);
        mCacheMutex.lock();
        auto ite = mCache.find(name);
        if(ite == mCache.end())
            mCache.insert(ite, std::make_pair(name, new unsigned char[1024]));
        // code here
        mCacheMutex.unlock();
        return true;
    }
private:
    mutable std::map<std::string, unsigned char*> mCache;
    mutable QMutex mCacheMutex;
};

int main(int argc, char *argv[])
{
    std::cout << "Test" << std::endl;
    TileCache cache;
#pragma omp parallel for shared(cache)
    for(int i = 0; i < 2048; ++i)
    {
        cache.fillBuffer("my buffer");
    }
    return 0;
}

我基本上是在问,如果总是调用lock()/unlock()(没有unlock()调用不匹配的lock()),是否有任何已知的理由认为这两种方式不等效于QMutexLocker,在某些情况下可能会表现出不同行为。


7
展示问题的一个真实小代码片段。 - user2249683
我再次运行了该应用程序,这次启用了运行时检查。它显示在函数作用域中声明的变量周围存在堆栈损坏。 - LeCoc
手动锁定/解锁不具备异常安全性。 - user2249683
4个回答

5
使用locker对象的优点在于它自动处理所有退出点,包括由异常引起的退出点。
通常忘记退出点或异常会导致互斥锁被锁定,程序挂起。
如果你遇到崩溃问题,则问题出现在其他地方,使用locker对象看到问题消失只是巧合(如果有崩溃,则程序肯定存在错误,如果没有崩溃,则不能说程序是正确的...特别是在像C++这样具有"未定义行为"概念的语言中)。
另一个不明显的优点是,使用作为第一条语句创建的locker对象可以确保在返回给调用者之前解锁。如果函数内部创建了依赖于互斥锁的其他对象,则这可能会产生影响。
void ok_func() {
    Locker mylock(mymutex);
    Obj myobj; // constructor and destructor require the lock
}

void buggy_func() {
    lock(mymutex);
    Obj myobj; // constructor and destructor require the lock
    unlock(mymutex);
    // Bug: myobj instance will be destroyed without the lock
}


void workaround_func() {
    lock(mymutex);
    {   // nested scope needed
        Obj myobj; // constructor and destructor require the lock
    }
    unlock(mymutex);
}

1
毕竟,这是一个在函数作用域内创建了一些具有非平凡析构函数的对象的问题。unlock() 调用在这些析构函数完成之前释放了互斥锁,导致了竞态条件。 非常感谢您的评论,它让我调查了该方法的内部代码,最终找到了问题所在。 - LeCoc
2
@LeCoc:很高兴听到你找到了问题所在。事实上,使用锁对象的另一个不明显的优点就是这个。由于销毁的顺序与构造的顺序相反,如果您在进入时创建锁定器,则还可以确保在离开函数之前(即在执行所有其他本地实例析构函数之后)解锁将发生。编辑答案以更清楚地显示此内容... - 6502

1

在遵照@6502的建议后,发现了问题所在。首先是一个小例子来重现这个问题:

class TileCache
{
    struct Culprit
    {
        Culprit(int& n) : mN(n) { ++mN; }
        ~Culprit() { --mN; }
    private:
        int& mN;
    };
public:
    int& fillBuffer(const std::string& name) const {
        //QMutexLocker lock(&mCacheMutex);
        mCacheMutex.lock();
        auto ite = mCache.find(name);
        if(ite == mCache.end())
            ite = mCache.insert(ite, std::make_pair(name, 0));
        Culprit culprit(ite->second);
        unsigned char somebuffer[1];
        somebuffer[ (ite->second -1) * 8192 ] = 'Q';
        mCacheMutex.unlock();
        return ite->second;
    }
private:
    mutable std::map<std::string, int> mCache;
    mutable QMutex mCacheMutex;
};
int main(int argc, char *argv[])
{
    TileCache cache;
#pragma omp parallel for shared(cache) num_threads(2)
    for(int i = 0; i < 2048; ++i)
    {
        int& n = cache.fillBuffer("my buffer");
        if(n != 0) throw std::logic_error("Buff");
    }
    return 0;
}

使用QMutexLocker和手动锁定/解锁的唯一区别在于,在使用锁定器的情况下,退出作用域时将调用解锁方法,并按照该作用域的创建相反顺序进行。这就是我没有看到的,填充缓冲区方法中的某些对象也会进行RIIA操作,因此填充缓冲区方法的结尾不应该是受保护部分的结尾。 通过使用互斥锁锁定器,最后调用解锁()方法,保护整个方法。当然,使用大括号来界定内部作用域,在该作用域内填充缓冲区方法将完成其工作,也可以起到作用。
int& fillBuffer(....) const {
    mCacheMutex.lock();
    {
        auto ite = ....
        ...
    }
    mCacheMutex.unlock();
}

但是真正的功能还有其他返回点,这会阻止这个解决方案的工作。

简而言之,我了解到,如果在某个作用域内创建的对象的析构函数也应该受到保护,那么基于作用域的互斥锁定器与手动调用lock()和unlock()是不等价的。 在这种情况下,作用域锁将起作用,但手动调用则不会

非常感谢所有试图帮助我的人,我很感激。


1
文档来看,QMutexLocker 稍微有些管理意义,因为它会在你超出作用域时自动解锁。如果你的 const 成员函数只有一条可能的返回路径,那么两种方法都可以。但是,如果这个函数变得更加复杂或者你在以后改变了设计,我建议使用类。

0

正如description所说,如果您的函数有许多退出点,则需要在每个退出点编写mutex.unlock()。但是,如果您使用互斥锁定器,则无需这样做。


该函数的所有“return”语句都在解锁之前。我现在对堆栈损坏的问题感到困惑,而且使用QMutexLocker时这种情况并不会发生。现在已经很晚了,我明天会进一步调查并在这里写下结果。 - LeCoc
看起来当你现在研究 QMutexLocker 的代码(我在看 Qt 4.2.2)时,它并不总是锁定或解锁。我无法真正理解它何时锁定或不锁定的逻辑,但也许你在另一个线程中有互斥量的副本,因为它是另一个实例,从未被重置。如果你在堆上分配你的互斥量会发生什么? - fassl

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