qt 多线程、信号槽、moveToThread等机制之拨乱反正

之所以要“拨乱反正”,是因为很多教科书上的说法,还有网页上的说法,都是错误的。

我没有看过qt源码,看过一些书籍,做过一些实验,说下我的理解。如有谬误,还请讨论。

首先来看看教科书上原版错误或者说混乱的说法:

你可以尝试用搜索引擎输入“qt connect第五个参数”,那么将得到下面主流的说法。但是这些人都是人云亦云,瞎抄一通,根本没有深入理解字段的说法。

第五个参数代表槽函数在哪个线程中执行 :
1)自动连接(AutoConnection),默认的连接方式,如果信号与槽,也就是发送者与接受者在同一线程,等同于直接连接;如果发送者与接受者处在不同线程,等同于队列连接。
2)直接连接(DirectConnection),当信号发射时,槽函数立即直接调用。无论槽函数所属对象在哪个线程,槽函数总在发送者所在线程执行,即槽函数和信号发送者在同一线程
3)队列连接(QueuedConnection),当控制权回到接受者所在线程的事件循环时,槽函数被调用。槽函数在接受者所在线程执行,即槽函数与信号接受者在同一线程
4)锁定队列连接(QueuedConnection)
Qt::BlockingQueuedConnection:槽函数的调用时机与Qt::QueuedConnection一致,不过发送完信号后发送者所在线程会阻塞,直到槽函数运行完。接收者和发送者绝对不能在一个线程,否则程序会死锁。在多线程间需要同步的场合可能需要这个。
5)单一连接(QueuedConnection)
Qt::UniqueConnection:这个flag可以通过按位或(|)与以上四个结合在一起使用。当这个flag设置时,当某个信号和槽已经连接时,再进行重复的连接就会失败。也就是避免了重复连接
 

拨乱反正点:“发送者”概念错误。上面红色加粗的字段,都是说法混淆、或者错误的说法。

“发送者”这个概念,存在相当大的错误或概念混淆。

何为发送者?从字面意思和文字意思来理解,是指(1)信号所在对象,还是指(2)“信号所在对象”所在的线程,还是指(3)当前调用emit触发信号的线程。

所有那些人云亦云的家伙,都没用实实在在搞明白,发送者是指(1)(2)(3)中的哪一个。有些人认为是(1),更多人认为是(2),但是真正严格的答案,我通过实验得出,却是(3)。

举个例子,有两个线程T1、T2,对象A在T1中,对象A有一个信号sig1,那么当在T2里面调用A的sig1时,该怎么解释上面说的,谁是信号的“发送者”?

具体用代码来表示。

/*我故意设计了这样一个例子来验证问题的严重性*/

class Thread :public QThread
{
public:
	Thread();
	Thread(Test *outObj) { m_outObj = outObj; };
	virtual ~Thread();
protected:
	void run() { 
		qDebug("new thread run in %p", currentThread());
		emit m_outObj->sig_test();
	}
private:
	Test * m_outObj;
};

class Test :
	public QObject
{
	Q_OBJECT;
public:
	Test();
	virtual ~Test();
signals:
	void sig_test();
public slots:
	void slot_test() { qDebug("Test slots in %p\n", QThread::currentThread()); }
};

int main(int argc, char *argv[])
{
	QCoreApplication a(argc, argv);
	qDebug("main in %p\n", QThread::currentThread());
	Test t;
	QObject::connect(&t, &Test::sig_test, &t, &Test::slot_test, Qt::DirectConnection);

	Thread *thread = new Thread(&t);
	thread->start();

	return a.exec();
}

上面代码只是示例。

上面代码就描述了前文的情况。我有两个线程T1(主线程)、T2(子线程thread),对象Test t 是在主线程T1中,当我们在T2的run里面发送信号 t.sig_test()时,谁是信号的发送者?注意为了抓出发送者,我用的是直连方式,因为在直连方式下,“槽函数的执行线程"与“发送者”在一个线程,这样通过槽函数在哪个线程,我们就知道谁是“发送者”。

看看输出:

main in 0x9036d8

new thread run in 0x909858
Test slots in 0x909858

没错!槽函数是在子线程中,那么说明“发送者”也是子线程! 因为我们用的是直连方式。

关于发送者是谁,第(1)中说法 “信号所在的对象”,是错误地。对象只是一个内存对象,它本身无法执行。这种说法一开始就排除。第(2)中说法“信号所在对象”所在的线程,也是错误。我们知道这里“信号所在对象”是 t ,而t 是在主线程定义的,那么按这个说法,发送者应该是主线程才对,但事实却不是,实践证明是子线程中发送的。

第(3)中说法,调用emit触发信号的线程。没错,我们是在run子线程中调用的emit,所以发送者就是子线程本身。事实证明也是如此。

如果把上面的connect的第五个参数修改为队列连接,槽函数在哪里执行,答案是什么?我们先说答案。

QObject::connect(&t, &Test::sig_test, &t, &Test::slot_test, Qt::QueuedConnection);

main in 0x8042a0

new thread run in 0x80a768
Test slots in 0x8042a0

槽函数在主线程中执行。这点是没有什么好怀疑的。因为队列模式下,槽函数是在接受者对象所在的线程中执行,与发送者无关。而接受者 t 就是在主线程中,那么槽函数就在主线程执行,这是没有疑问的。

我们一定要有一个概念,槽函数在什么地方执行,是动态决定的,不是写connect时决定的。如果是写connect时就决定,那么上面的写法,总是给人一种都是在主线程中执行的错觉。

三个核心要素,来决定槽函数在哪个线程调用。

(1) 调用emit 发送信号的线程。

(2)接受者对象所在的线程。

(3)connect的第五个参数。

最容易忽略,而且最容易出错的是第一个要素。

那么为了表达准确,我们现在用准确的语句,来重写上面的准则。

第五个参数代表槽函数在哪个线程中执行 :
1)自动连接(AutoConnection),默认的连接方式,如果信号与槽,也就是“发送信号的线程”与“接受者所在的线程“是同一线程,等同于直接连接;如果“发送信号的线程”与“接受者所在的线程”不是一个线程,等同于队列连接。
2)直接连接(DirectConnection),当信号发射时,槽函数立即直接调用。无论槽函数所属对象在哪个线程,槽函数总在“发送信号的线程”中执行,即槽函数和“信号发送线程”在同一线程
3)队列连接(QueuedConnection),当控制权回到接受者所在线程的事件循环时,槽函数被调用。槽函数在接受者所在线程执行,即槽函数与"信号接受者所在线程"在同一线程
4)锁定队列连接(QueuedConnection)
Qt::BlockingQueuedConnection:槽函数的调用时机与Qt::QueuedConnection一致,不过发送完信号后“发送信号的线程”会阻塞,直到槽函数运行完。“接收者所在线程”和“发送信号的线程”绝对不能在一个线程,否则程序会死锁。在多线程间需要同步的场合可能需要这个。
5)单一连接(QueuedConnection)
Qt::UniqueConnection:这个flag可以通过按位或(|)与以上四个结合在一起使用。当这个flag设置时,当某个信号和槽已经连接时,再进行重复的连接就会失败。也就是避免了重复连接。

我们再次重申:“发送信号的线程”就是指代码中调用 emit 信号时的执行线程;而绝不是“信号所在对象“所属的线程。

“信号所在对象”所属的线程,是创建该对象的线程。“信号所在对象”所属的线程,不是一层不变的,可以通过moveToThread改变。

上文可谓字字珠玑。许多糊涂蛋把“发送信号的线程” 搞错为“发送者所在线程”,后者干脆模糊不清用“发送者”代替。这些是大错特错的。简直是以讹传讹,基本上目前我看到的所有已有版本都是错误的。

关于:moveToThread

该函数会改变对象所在的线程,对象所在的线程使用Qobject::thread()函数查看,当前线程使用currentThread()查看。二者容易混淆,这是两个不同的概念。该函数改变的是前者。

还是上面的例子,我们把t移动到子线程中去,还是采用队列连接方式。由于队列连接时,槽函数在接受者所在线程执行,与发送者线程无关,所以这时槽函数就会在子线程执行。

代码如下:

class Thread :public QThread
{
public:
	Thread();
	Thread(Test *outObj) { m_outObj = outObj; };
	virtual ~Thread();
protected:
	void run() { 
		qDebug("new thread run in %p", currentThread());
		emit m_outObj->sig_test();
		int i = 0;
		while (i++ < 10) /*延时退出,否则槽函数没有机会执行*/
		{
			QCoreApplication::processEvents();
		}
	}
private:
	Test * m_outObj;
};

int main(int argc, char *argv[])
{
	QCoreApplication a(argc, argv);
	qDebug("main in %p\n", QThread::currentThread());
	Test *t = new Test;
	QObject::connect(t, &Test::sig_test, t, &Test::slot_test, Qt::QueuedConnection);


	qDebug("t now in %p\n", t->thread());
	Thread *thread = new Thread(t);
	thread->start();
	t->moveToThread(thread);
	qDebug("t now in %p\n", t->thread());

	return a.exec();
}
/*Test代码没有修改,不贴出*/

运行结果毫无意外地,t moveToThread后,其所在的线程发生了变化。

main in 0x2241b0

t now in 0x2241b0

t now in 0x228d10

new thread run in 0x228d10
Test slots in 0x228d10

最终槽函数是在子线程中执行的。

注意我们在子线程中加入了延时处理,见代码中的注释。如果不加的话,那么提交emit 信号后,子线程继续退出。后面就没有机会再去执行信号的槽函数了。不信的话,你去掉上面的延时函数,槽函数就无法正常打印出了。

原因是因为使用的队列连接方式,需要回到子线程的事件循环中后,才会执行槽函数。但是子线程提交信号后直接退出,再也不会回到它的事件循环中,就没有机会执行槽函数拉。

这是特别需要注意的一点。直接连接时,不需要等到进入事件循环中才能执行槽函数,而且直接就立即调用槽函数啦。而队列连接,会有一个延时,信号在事件队列中排队,只有线程进入到事件消息处理中,才能去执行槽函数。

猜你喜欢

转载自blog.csdn.net/peterbig/article/details/99722345