【写在前面】
最近在用 QMutex 的时候,偶然出现了死锁。
这个场景并不常见,并且可能是自己的习惯导致。
不过既然出现了,那么在以后编程中就需要注意。
【正文开始】
先直接上代码吧:
#include <QApplication>
#include <QMutex>
#include <QThread>
#include <QWidget>
class Test : public QWidget
{
Q_OBJECT
public:
Test(QWidget *parent = nullptr) : QWidget(parent) {
connect(this, &Test::signalA, this, &Test::testB);
}
void test() {
m_mutex.lock();
//do something
emit signalA();
m_mutex.unlock();
}
public slots:
void testB() {
m_mutex.lock();
//do something
//QThread::msleep(1000);
m_mutex.unlock();
}
signals:
void signalA();
private:
QMutex m_mutex;
};
int main(int argc, char **argv)
{
QApplication a(argc, argv);
Test t;
t.show();
t.test();
return a.exec();
}
#include "main.moc"
这里要提一下,QObject::connect() 的连接类型默认是 Qt::AutoConnection ,在同一线程中即为 Qt::DirectConnection。
因此,在代码里,test() 函数实际为:
void test() {
m_mutex.lock();
signalA() call -> testB() {
m_mutex.lock();
//do something
m_mutex.unlock();
}
m_mutex.unlock();
}
可以看到,互斥锁 m_mutex 连续锁定( lock() )了两次,而第二次锁定将导致当前线程被挂起(例子中为主线程),并且,没有任何地方能够解锁( unlock() ),此时也就产生了死锁。
出现这种情况的原因还是我太习惯用信号了,实际上,如果 testB() 是另外一个线程的函数,那么 signalA() 将是以 Qt::QueueConnection 的方式连接到 testB() ,死锁就不会产生:
#include <QApplication>
#include <QMutex>
#include <QThread>
#include <QDebug>
class Test : public QThread
{
Q_OBJECT
public:
Test(QObject *parent = nullptr) : QThread(parent) {
connect(this, &Test::signalA, this, &Test::testB);
}
void test() {
m_mutex.lock();
//do something
emit signalA();
m_mutex.unlock();
qDebug() << QThread::currentThreadId() << " test unlock()";
}
public slots:
void testB() {
m_mutex.lock();
//do something
m_mutex.unlock();
qDebug() << QThread::currentThreadId() << " testB unlock()";
}
signals:
void signalA();
protected:
void run() {
test();
}
private:
QMutex m_mutex;
};
int main(int argc, char **argv)
{
QApplication a(argc, argv);
Test t;
t.start();
return a.exec();
}
#include "main.moc"
testB() 在主线程,test() call signalA() 则处于 run() 函数也就是子线程中。
其运行结果如图:
互斥锁正确的解锁了两次,即消除了死锁,当然,这并不是正确的解决方案 ( 因为利用了 QueueConnection 进入了事件循环( 队列 ),只是表面上是消除了,与多线程无关 )。
实际上,真正的解决方案相当简单:只需将 emit signalA() 移出 { lock() unlock() } 即可。
同样,更需要注意的是:QMutexLocker 。
还是最开始的场景,这次使用 QMutexLocker:
#include <QApplication>
#include <QWidget>
#include <QMutex>
#include <QThread>
class Test : public QWidget
{
Q_OBJECT
public:
Test(QWidget *parent = nullptr) : QWidget(parent) {
connect(this, &Test::signalA, this, &Test::testB);
}
void test() {
QMutexLocker locker(&m_mutex);
//do something
emit signalA();
}
public slots:
void testB() {
QMutexLocker locker(&m_mutex);
//do something
}
signals:
void signalA();
private:
QMutex m_mutex;
};
int main(int argc, char **argv)
{
QApplication a(argc, argv);
Test t;
t.show();
t.test();
return a.exec();
}
#include "main.moc"
结果仍是产生了死锁。
当然,这也是 QMutexLocker 这类 Locker 的弊端 (在析构中 unlock),所以要谨慎啊喂(#`O′)。
【结语】
最后,一点建议:
- QMutex 的 lock() 和 unlock() 之间尽量只包含对共享变量的读 / 写操作,不要有任何其他函数调用(除非保证不会产生死锁)。
- QMutexLocker,尽量自己控制作用域(看得见的析构):{ QMutextLocker(&mutex); },在作用域结束时析构。
- 一定,一定,一定要谨慎。