C++ 使用互斥锁和手动锁定互斥锁的区别
具体来说,在const成员函数中,如果我在其开头使用C++ 使用互斥锁和手动锁定互斥锁的区别,c++,multithreading,qt,C++,Multithreading,Qt,具体来说,在const成员函数中,如果我在其开头使用mutex.lock(),并且在返回之前使用mutex.unlock(),则在OpenMP循环中运行时会崩溃。但是如果我在函数的开头用一个QMutexLocker(&mutex)替换这两个调用,它就会运行得很顺利。 VisualStudio2010,Qt4.8。 我希望这两个代码是等效的,但它们显然不是。我错过了什么 编辑: 虽然这不会重现问题,但有一个小例子: class TileCache { public: bool fillBu
mutex.lock()
,并且在返回之前使用mutex.unlock()
,则在OpenMP循环中运行时会崩溃。但是如果我在函数的开头用一个QMutexLocker(&mutex)
替换这两个调用,它就会运行得很顺利。
VisualStudio2010,Qt4.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;
}
class-TileCache
{
公众:
bool fillBuffer(const std::string&name)const{
//QMutexLocker锁(&mcacheutex);
mCacheMutex.lock();
auto ite=mCache.find(名称);
如果(ite==mCache.end())
insert(ite,std::make_pair(名称,新的无符号字符[1024]);
//代码在这里
mCacheMutex.unlock();
返回true;
}
私人:
可变std::map-mCache;
可变QMutex-mCacheMutex;
};
int main(int argc,char*argv[])
{
std::cout正如前面所说,如果您有一个具有多个出口点的函数,那么您需要编写mutex.unlock()
到每个退出点。但是如果您使用互斥锁,则不必执行此操作。从中可以看出,QMutexLocker在某种程度上是托管的,这意味着它将在您超出范围时自动神奇地解锁。如果您的const成员函数只有一个可能的返回路径,则任何一种方法都可以。但是,如果此函数ecomes更复杂,或者如果您稍后更改设计,我只会使用该类。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);
}
好的,在遵循@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;
}
但实际函数还有一些其他返回点,这会阻止此解决方案工作
博士
因此我了解到,是的,与手动调用lock()和unlock()相比,如果在该作用域内创建的对象的析构函数也应该受到保护,那么使用基于作用域的互斥锁是不等价的。在这种情况下,作用域锁将起作用,但手动调用不会起作用
非常感谢所有试图帮助我的人,我对此表示感谢。一个非常小的代码片段,显示了问题所在。我再次运行了应用程序,这次启用了运行时检查。它显示了函数范围内声明的变量周围的堆栈损坏。手动锁定/解锁不是异常安全的。函数具有所有的“返回”“前面有unlock语句。我现在对堆栈损坏的问题感到困惑,而且QMutexLocker没有出现这种情况。现在已经晚了,我明天会进一步调查,并将结果写在这里。看起来就像是在调查QMutexLocker的代码时(我正在查看qt 4.2.2)现在,我认为它并不总是锁定或解锁。我无法真正理解它锁定或不锁定时背后的逻辑,但可能你在另一个线程中也有一个互斥体副本,因为它是另一个实例,所以永远不会重置。如果你在堆上分配互斥体会发生什么?毕竟,这是一些非triv对象的问题在函数作用域内创建的析构函数call在这些析构函数完成之前释放了互斥锁,造成了竞争条件。非常感谢您的评论,它让我调查了该方法的内部代码,最终找到了问题。@LeCoc:很高兴听到您发现了问题。事实上,这是使用locker对象的另一个不明显的优势。因为销毁是反向进行的构造顺序如果你在回车时创建了锁,你也可以保证在离开函数之前解锁(也就是说,在执行所有其他本地实例析构函数之后)。编辑答案以更清楚地显示这一点。。。
int& fillBuffer(....) const {
mCacheMutex.lock();
{
auto ite = ....
...
}
mCacheMutex.unlock();
}