Qt マルチスレッド、シグナル スロット、moveToThread、および状況を正しく設定するためのその他のメカニズム

「正しく設定する」理由は、教科書やウェブサイトの記述の多くが間違っているからです。

私はqtのソースコードを読んだことはありませんが、いくつかの本を読んだり実験したりしたので、私の理解を共有したいと思います。誤りがある場合も議論してください。

 

まず、教科書の元の間違いや混乱を招く記述を見てみましょう。

検索エンジンを使用して「qt connect 5 番目のパラメータ」と入力すると、次のような主流のステートメントが表示されます。しかし、これらの人々は他人の言うことをコピーし、盲目的にコピーしており、その分野についてまったく深く理解していません。

5 番目のパラメーターは、スロット関数がどのスレッドで実行されるかを表します。
1) AutoConnection、デフォルトの接続方法。信号とスロット、つまり送信者受信者が同じスレッドにある場合は、直接接続と同等です。送信者 受信者とは別のスレッドにあり、キュー接続に相当します。
2) 直接接続 (DirectConnection) 信号が発信されると、すぐにスロット関数が直接呼び出されます。スロット関数のオブジェクトがどのスレッドにあるかに関係なく、スロット関数は常に送信側が配置されているスレッドで実行されます。つまり、スロット関数とシグナル送信側は
同じスレッドにあります。 3) QueuedConnection、コントロールが存在する場合レシーバーが配置されているスレッドのイベント ループに戻ると、スロット関数が呼び出されます。スロット関数は受信側のスレッドで実行されます。つまり、スロット関数とシグナル受信側が同じスレッドになります。
4) キュー接続のロック(QueuedConnection) Qt::BlockingQueuedConnection: スロット関数の呼び出しタイミングは同じですQt::QueuedConnection と同様ですが、
シグナルの送信後に送信されます。後者が配置されているスレッドは、スロット関数の実行が終了するまでブロックされます。受信者と送信者が同じスレッド内に存在してはなりません。そうしないと、プログラムがデッドロックしてしまいます。これは、複数のスレッド間の同期が必要な場合に必要になる場合があります。
5) 単一接続 (QueuedConnection)
Qt::UniqueConnection: このフラグは、ビット単位または (|) によって上記の 4 つと組み合わせて使用​​できます。このフラグが設定されている場合、信号とスロットがすでに接続されている場合、繰り返し接続は失敗します。繰り返しの接続を避けるためです
 

正しく設定するには: 「送信者」という概念が間違っています。上の赤​​と太字のフィールドはすべて、紛らわしい、または間違った記述です。

「送信者」の概念については、かなりの誤りまたは概念的な混乱があります。

送信者とは何ですか? 文字通りの意味と文字通りの意味から、これは、(1) シグナルが配置されているオブジェクト、または (2) 「シグナルが配置されているオブジェクト」が配置されているスレッド、または (3) 現在 Emit を呼び出しているスレッドを指します。信号をトリガーします。

発言する人たちにとって、送信者が (1)(2)(3) のどれを指しているのかを本当に理解することは無駄です。(1)だと思う人もいますし、(2)だと思う人のほうが多いですが、私が実験で得た本当に厳密な答えは(3)です。

 

たとえば、2 つのスレッド 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();
}

上記のコードは単なる例です。

上記のコードは以前の状況を説明しています。2 つのスレッド T1 (メイン スレッド) と T2 (サブスレッド スレッド) があります。オブジェクト Test t はメイン スレッド T1 にあります。T2 の実行でシグナル t.sig_test() を送信すると、送信者は誰になりますか信号?? 送信者を捕捉するために、直接接続方法を使用することに注意してください。直接接続方法では、「スロット関数実行スレッド」と「送信者」が同じスレッド内にあるため、スロット関数が誰に属しているかがわかります。どのスレッドが「送信者」なのか。

出力を見てみましょう。

0x9036d8のメイン

新しいスレッドは 0x909858 で実行されます
0x909858 のテストスロット

それは正しい!スロット関数は子スレッドにあるので、「送信者」も子スレッドということになります! 直接接続方式を使用しているためです。

送信者が誰かについて、(1)の「信号が存在する物体」というのは間違いです。オブジェクトは単なるメモリ オブジェクトであり、それ自体を実行することはできません。この議論は最初から除外されていました。(2)の「信号が置かれているオブジェクト」という記述も間違っています。「シグナルが存在するオブジェクト」が t であり、t がメイン スレッドで定義されていることはわかっています。この記述によれば、送信者はメイン スレッドであるはずですが、実際はそうではありません。サブスレッドで送信されました。

(3) では、emit を呼び出すスレッドがシグナルをトリガーします。そうです、実行サブスレッドで Emit を呼び出すので、送信者はサブスレッド自体になります。同じことが真実であることがわかります。

 

上記の connect の 5 番目のパラメーターが queue 接続に変更された場合、slot 関数はどこで実行され、その答えは何でしょうか? 答えから始めましょう。

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

0x8042a0のメイン

新しいスレッドは 0x80a768 で実行されます
テストスロットは 0x8042a0 で実行されます

スロット関数はメインスレッドで実行されます。これについては疑いの余地がありません。キュー モードでは、スロット関数は受信側オブジェクトが配置されているスレッドで実行され、送信側とは何の関係もないためです。そして、受信者 t はメインスレッド内にあり、スロット関数はメインスレッド内で実行されます。これには疑いの余地がありません。

スロット関数が実行される場所は、connect を記述するときではなく、動的に決定されるという概念が必要です。connectを書くときに決めてしまうと、上記の書き方では常にすべてメインスレッドで実行されているような錯覚を与えてしまいます。

3 つのコア要素は、スロット関数が呼び出されるスレッドを決定します。

(1) シグナルを送信するために Emit を呼び出したスレッド。

(2) レシーバオブジェクトが配置されているスレッド。

(3) connect の 5 番目のパラメータ。

最も見落としやすく、最も間違いが起こりやすいのは、最初の要素です。

そこで、正確に表現するために、現在は上記のガイドラインを正確な文章で書き直しています。

5 番目のパラメーターは、スロット関数が実行されるスレッドを表します。
1) AutoConnection (AutoConnection)、シグナルとスロットの場合のデフォルトの接続方法、つまり、「シグナルを送信するスレッド」と「シグナルを送信するスレッド」 「受信者が位置するスレッド」が同じスレッドである場合は、直接接続と同等ですが、「シグナルを送信するスレッド」と「受信者が位置するスレッド」が同じスレッドでない場合は、キュー接続と同等です。
2) 直接接続 (DirectConnection) 信号が発信されると、すぐにスロット関数が直接呼び出されます。スロット関数のオブジェクトがどのスレッドにあっても、スロット関数は常に「シグナルを送信するスレッド」で実行されます。つまり、スロット関数と「シグナル送信スレッド」は同じスレッド内にあります。
3) QueuedConnection 、制御が受信側に戻るとき スレッドのイベント ループのときに、スロット関数が呼び出されます。スロット関数は受信側のスレッドで実行されます。つまり、スロット関数は
スレッド「シグナル受信スレッド」とシグナルを送信したスレッド」はスロット関数が実行されるまでブロックされます。実行を終了します。「受信者が配置されているスレッド」と「シグナルを送信するスレッド」は同じスレッド内に存在してはなりません。そうしないと、プログラムがデッドロックしてしまいます。これは、複数のスレッド間の同期が必要な場合に必要になる場合があります。5) 単一接続 (QueuedConnection) Qt::UniqueConnection: このフラグは、ビット単位または (|) によって上記の 4 つと組み合わせて使用​​できます。このフラグが設定されている場合、信号とスロットがすでに接続されている場合、繰り返し接続は失敗します。それは、繰り返しの接続を避けるためです。


もう一度繰り返します: 「シグナルを送信するスレッド」とは、コード内で発行シグナルが呼び出されたときの実行スレッドを指します。これは、「シグナル オブジェクト」が属するスレッドではありません。

「シグナルが存在するオブジェクト」が属するスレッドは、そのオブジェクトを作成したスレッドです。「信号が存在するオブジェクト」が属するスレッドは、1 つのレイヤーで一定ではなく、moveToThread によって変更できます。

以上のことは雄弁であると言えます。多くの愚か者は「シグナルを送信したスレッド」と「送信者がいるスレッド」を混同しており、後者は単純に曖昧で「送​​信者」に置き換えられています。これらは完全に間違っています。それは単なるデマであり、基本的に私がこれまでに見た既存のバージョンはすべて間違っています。

 

概要:moveToThread

この関数は、オブジェクトが配置されているスレッドを変更します。オブジェクトが配置されているスレッドを表示するには Qobject::thread() 関数を使用し、現在のスレッドを表示するには currentThread() を使用します。この 2 つは異なる概念であるため、混同されやすいです。この関数は前者を変更します。

上記の例でも、 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代码没有修改,不贴出*/

操作の結果は驚くことではありません。moveToThread の後、そのスレッドが変更されています。

0x2241b0のメイン

今は0x2241b0にあります

今は0x228d10です

新しいスレッドは 0x228d10 で実行されます
0x228d10 でスロットをテストします

最後のスロット関数は子スレッドで実行されます。

子スレッドに遅延処理を追加していることに注意してください。コード内のコメントを参照してください。これが追加されていない場合、エミット信号を送信した後、子スレッドは終了し続けます。後でシグナルのスロット機能を実行する機会はありません。信じられないかもしれませんが、上記の遅延関数を削除すると、スロット関数は正常に印刷されなくなります。

その理由は、使用されているキュー接続方法により、スロット関数は子スレッドのイベント ループに戻った後にのみ実行されるためです。ただし、子スレッドはシグナルの送信後すぐに終了し、イベント ループに戻ることはないため、スロット関数のプルを実行する機会はありません。

この点は特に注意が必要です。直接接続する場合、スロット関数を実行するためにイベント ループに入るまで待つ必要はなく、スロット関数を直接呼び出します。また、キュー接続には遅延が発生し、シグナルはイベント キューにキューイングされ、スレッドがイベント メッセージ処理に入ったときにのみスロット関数を実行できます。

おすすめ

転載: blog.csdn.net/peterbig/article/details/99722345