今回は、QObject::moveToThread()
の話。
ソースは、githubに上げてある。
QThreadを使うと言うと、virtual void QThread::run()
をオーバーライドしたクラスを作って使うと言う頭しかなかったんだけど、run()
にはデフォルト実装があり、こいつはexec()
を呼ぶと書いてある。
exec()
は何をするかと言うと、イベントループを回す。
これで何ができるかと言うと、前回ちょっと触れた QObject::moveToThread()
でオブジェクトのイベントを処理するスレッドを変えることができる。
QObjectにはThreadのaffinityと言う概念があり、そのオブジェクトのイベントを処理するスレッドが決まっている。
affinityは以下のルールで決定される。
- 親がいる場合は親のaffinity thread
- 親を指定しなかった場合はオブジェクトを生成したときのcurrent thread
何も指定しないでプログラムを書いた場合は、メインスレッド(QApplication::exec
を呼んだスレッド)になる。
これを、意図的に変えてやるのが QObject::moveToThread()
である。
moveToThread()
の条件は、以下のようになる。
-
moveToThread()
しようとしているオブジェクトが親を持たないこと。 -
moveToThread()
しようとしているオブジェクトの現在のaffinity threadがcurrent threadであること。
QObjectの親子関係があるオブジェクトは、全て同じthread affinity を持たないといけないので、affinity threadが違うオブジェクト同士をsetParent()
等で新しく親子にすることはできない。
なんだかわからなくなってきたのでサンプルの説明を。
OtherTaskObject
新しいクラスとして、OtherTaskObjectが登場する。
これは、時間がかかる処理を実行する演算クラスで、別スレッドで実行されることを想定している。
job1, job2, longJobの3つのジョブを実行するための関数として、startJobInternal1()
, startJobInternal2()
, longJob()
がある。
job1, job2はそれぞれタイマを起動するだけ、longJobはsleepするだけの実装になっている。
タイマとして、QTimerのインスタンスtimer1, timer2を持っている。
timer1は親オブジェクトなし、timer2はOtherTaskObjectを親としている。
後述するが、QTimer
のstart()/stop()
はQTimer
のaffinity threadからしか呼ぶことができないので、スレッドを切り替えて呼び出すためのstartJob1()
, startJob2()
がある。
中身は、QMetaObject::invokeMethod()
を使ってQt::QueuedConnection
で各internal関数を呼ぶようになっている。
MainWindow
MainWindow
は、コンストラクタでOtherTaskObject
のインスタンスを生成し、OtherTaskThread
にmoveToThread()
し、OtherTaskThread
をstart
する。
MainWindow
には button 1 〜 button 5 の5つのボタンがある。
button 1
button 1 を押すと OtherTaskObject::startJob1()
を呼び出す。
このときのスレッドはメインスレッドである。
OtherTaskObject::startJob1()
はQMetaObject::invokeMethod()
でOtherTaskObject::startJobInternal1()
を呼び出すが、キューに積むだけである。
OtherTaskThreadがキューからinvoke要求を取り出し、OtherTaskObject::startJobInternal1()
を呼び出す。
OtherTaskObject::startJobInternal1()
はtimer1.start()を呼ぶが、timer1のaffinity threadはOtherTaskThreadではなくメインスレッドなので、タイマの起動に失敗する。
QTimer以外にも、QUdpSocketなど Qtが内部的にthreadを生成するようなクラスは、threadを越えて呼び出しができないことがある。
コメントで指摘をいただきましたが、「内部的にthreadを生成する」は間違いでした。
とりあえず「threadを越えて呼び出しができないことがある」は正しいので、基本的には別のthread affinityを持つオブジェクトの関数は直接呼ばない方が良い(signal/slotで繋ぐか、invokeMethodを使う)、くらいで覚えておけば良いかと。
12:25:25.480 00007fff7b368300 call otherTaskObject->startJob1()
12:25:25.480 00007fff7b368300 OtherTaskObject::startJob1 called
12:25:25.480 000000011384e000 OtherTaskObject::startJobInternal1 called
QObject::startTimer: Timers cannot be started from another thread
button 2
button 2 を押すと OtherTaskObject::startJobInternal1()
を呼び出す。
このときのスレッドはメインスレッドである。
OtherTaskObject::startJobInternal1()
はtimer1.start()を呼び、タイマが起動する。
タイマがタイムアウトすると、OtherTaskThreadでタイムアウトハンドラtimeout()
が呼び出され、signalをemitする。
このsignalはMainWindow
のキューに積まれ、メインスレッドでslotが呼び出される。
12:26:00.811 00007fff7b368300 call otherTaskObject->startJobInternal1()
12:26:00.811 00007fff7b368300 OtherTaskObject::startJobInternal1 called
12:26:03.870 000000011384e000 OtherTaskObject::timeout called
12:26:03.870 00007fff7b368300 receive finished signal
button 3
button 3 を押すと OtherTaskObject::startJob2()
を呼び出す。
このときのスレッドはメインスレッドである。
OtherTaskObject::startJob2()
はQMetaObject::invokeMethod()
でOtherTaskObject::startJobInternal2()
を呼び出すが、キューに積むだけである。
OtherTaskThreadがキューからinvoke要求を取り出し、OtherTaskObject::startJobInternal2()
を呼び出す。
OtherTaskObject::startJobInternal2()
はtimer2.start()を呼ぶが、timer2のaffinity threadはOtherTaskThreadなので、起動に成功する。
タイマがタイムアウトすると、OtherTaskThreadでタイムアウトハンドラtimeout()
が呼び出され、signalをemitする。
このsignalはMainWindow
のキューに積まれ、メインスレッドでslotが呼び出される。
12:26:34.734 00007fff7b368300 call otherTaskObject->startJob2()
12:26:34.734 00007fff7b368300 OtherTaskObject::startJob2 called
12:26:34.734 000000011384e000 OtherTaskObject::startJobInternal2 called
12:26:37.869 000000011384e000 OtherTaskObject::timeout called
12:26:37.869 00007fff7b368300 receive finished signal
button 4
button 4 を押すと OtherTaskObject::startJobInternal2()
を呼び出す。
このときのスレッドはメインスレッドである。
OtherTaskObject::startJobInternal2()
はtimer2.start()を呼ぶが、timer2のaffinity threadはメインスレッドではなくOtherTaskThreadなので、タイマの起動に失敗する。
12:27:03.488 00007fff7b368300 call otherTaskObject->startJobInternal2()
12:27:03.488 00007fff7b368300 OtherTaskObject::startJobInternal2 called
QObject::startTimer: Timers cannot be started from another thread
button 5
button 5 を押すと、OtherTaskObject::startLongJob()
を呼び出す。
OtherTaskObject::startLongJob()
はQMetaObject::invokeMethod()
でOtherTaskObject::longJob()
を呼び出すが、キューに積むだけである。
OtherTaskThreadがキューからinvoke要求を取り出し、OtherTaskObject::longJob()
を呼び出す。
OtherTaskObject::longJob()
は5秒間sleepし、signalをemitする。
このsignalはMainWindow
のキューに積まれ、メインスレッドでslotが呼び出される。
12:27:30.435 00007fff7b368300 call otherTaskObject->startLongJob()
12:27:30.435 00007fff7b368300 OtherTaskObject::startLongJob called
12:27:30.435 000000011384e000 OtherTaskObject::longJob called
12:27:35.438 000000011384e000 OtherTaskObject::longJob finished
12:27:35.438 00007fff7b368300 receive finished signal
button 3 を連打
button 3 を1秒置きに3回押してみる。
12:29:00.611 00007fff7b368300 call otherTaskObject->startJob2()
12:29:00.611 00007fff7b368300 OtherTaskObject::startJob2 called
12:29:00.611 000000011384e000 OtherTaskObject::startJobInternal2 called
12:29:01.656 00007fff7b368300 call otherTaskObject->startJob2()
12:29:01.656 00007fff7b368300 OtherTaskObject::startJob2 called
12:29:01.656 000000011384e000 OtherTaskObject::startJobInternal2 called
12:29:02.747 00007fff7b368300 call otherTaskObject->startJob2()
12:29:02.747 00007fff7b368300 OtherTaskObject::startJob2 called
12:29:02.747 000000011384e000 OtherTaskObject::startJobInternal2 called
12:29:05.869 000000011384e000 OtherTaskObject::timeout called
12:29:05.869 00007fff7b368300 receive finished signal
OtherTaskObject::startJob2()
とOtherTaskObject::startJobInternal2()
がそれぞれ3回呼び出されているが、OtherTaskObject::timeout()
は最後のOtherTaskObject::startJobInternal2()
から3秒後に1回のみ呼び出されている。
これは、QTimer::start()
がタイマのリスタートをしているためである。
実際の演算処理を書く場合は、演算中などの状態を持ち、排他制御をする必要があることがわかる。
button 5 を連打
button 5 を1秒置きに3回押してみる。
12:33:32.572 00007fff7b368300 call otherTaskObject->startLongJob()
12:33:32.572 00007fff7b368300 OtherTaskObject::startLongJob called
12:33:32.572 000000011384e000 OtherTaskObject::longJob called
12:33:33.292 00007fff7b368300 call otherTaskObject->startLongJob()
12:33:33.292 00007fff7b368300 OtherTaskObject::startLongJob called
12:33:34.135 00007fff7b368300 call otherTaskObject->startLongJob()
12:33:34.135 00007fff7b368300 OtherTaskObject::startLongJob called
12:33:37.573 000000011384e000 OtherTaskObject::longJob finished
12:33:37.573 00007fff7b368300 receive finished signal
12:33:37.573 000000011384e000 OtherTaskObject::longJob called
12:33:42.577 000000011384e000 OtherTaskObject::longJob finished
12:33:42.577 00007fff7b368300 receive finished signal
12:33:42.577 000000011384e000 OtherTaskObject::longJob called
12:33:47.580 000000011384e000 OtherTaskObject::longJob finished
12:33:47.580 00007fff7b368300 receive finished signal
OtherTaskObject::startLongJob()
は、ボタンを押したタイミングで3回呼ばれているが、実際のOtherTaskObject::longJob()
の呼び出しは前の実行が終わってから、順番に呼ばれていることがわかる。
これは、OtherTaskObject::longJob()
の実行はotherTaskThreadで行われるため、処理中は次のイベント処理ができないためである。
buton 3を押した後にbutton 5を押す
button 3を押して、3秒以内にbutton 5を押してみる。
12:37:59.460 00007fff7b368300 call otherTaskObject->startJob2()
12:37:59.460 00007fff7b368300 OtherTaskObject::startJob2 called
12:37:59.460 000000011384e000 OtherTaskObject::startJobInternal2 called
12:38:00.727 00007fff7b368300 call otherTaskObject->startLongJob()
12:38:00.728 00007fff7b368300 OtherTaskObject::startLongJob called
12:38:00.728 000000011384e000 OtherTaskObject::longJob called
12:38:05.731 000000011384e000 OtherTaskObject::longJob finished
12:38:05.731 00007fff7b368300 receive finished signal
12:38:05.732 000000011384e000 OtherTaskObject::timeout called
12:38:05.732 00007fff7b368300 receive finished signal
button 3を押して3秒のタイマをスタートしたタイミングは 12:38:00.727 だが、タイマのタイムアウトハンドラが呼び出されるのは 12:38:05.732 と5秒程度間があいてしまっている。
時間がかかる処理をメインスレッドと分離するために QObject::moveToThread()
を使う場合、そのスレッドが扱う処理に時間がかかる処理があると、イベント処理が期待通りに実行されないことがわかる。
メインスレッド(GUI処理スレッド)とスレッドを分離したからと言って、slot で時間がかかる処理をするのはまずいケースがあるので、スレッドの設計は気を遣う必要があると言うこと。