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

时间:2015-06-03 17:52:39

标签: c++ multithreading qt

具体来说,在const成员函数中,如果我在它的开头使用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()(没有lock()调用而没有匹配{{在某些情况下,1}}}的行为可能与unlock()不同。

4 个答案:

答案 0 :(得分:3)

更衣室对象的优点是自动处理所有出口点,包括由例外引起的出口点。

通常会忘记退出点或异常,但只是让互斥锁被锁定,症状就是程序只是挂起。

如果你遇到了崩溃,那么问题出在其他地方,而且当你使用一个更衣室对象时你看到问题消失的事实只是一个巧合(如果发生了崩溃,程序肯定是错误的,如果没有崩溃你不能说程序是正确的...尤其是像C ++这样的语言有&#34;未定义的行为&#34;)。

修改

另一个非显而易见的优点是,使用作为第一个语句创建的锁定对象,您可以保证在返回调用者之前解锁将作为最后一件事发生。如果在函数内部创建了依赖于互斥锁的其他对象,则可能会有所不同:

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 :(得分:1)

documentation开始,似乎QMutexLocker有点管理意味着当你超出范围时它会自动解锁。如果你的const成员函数只有一个可能的返回路径,那么任何一种方法都可以。但是,如果此函数变得更复杂或者您稍后更改了设计,我只会使用该类。

答案 2 :(得分: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和手动锁定/解锁之间的唯一区别是,在更衣室的情况下,退出范围时将按照与该范围创建相反的顺序调用unlock方法。这就是我没看到的,fillBuffer方法中的一些对象也做RIIA,所以fillBuffer方法的结尾不应该是受保护部分的结尾。 通过使用互斥锁,最后调用unlock()方法,保护整个方法。当然,它也可以使用大括号来划分内部作用域,其中所有fillBuffer方法都将在其中起作用:

int& fillBuffer(....) const {
    mCacheMutex.lock();
    {
        auto ite = ....
        ...
    }
    mCacheMutex.unlock();
}

但是真正的函数有一些其他的返回点,这会阻止这个解决方案的工作。

TL; DR 所以我了解到,是的,如果在该范围内创建的对象的析构函数也应该受到保护,那么与手动调用lock()和unlock()相比,它不等同于基于作用域的互斥锁。 在这种情况下,范围锁定将起作用,但手动调用不会

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

答案 3 :(得分:0)

正如description所说,如果你有一个具有许多退出点的函数,则需要将mutex.unlock()写入每个出口点。但是如果你使用互斥锁,你不必这样做。