┌──────────┐ │ イベントキュー・ │┌─────────┐┌─────────┐┌─────────┐ │ ディスパッチャ ││ QObject::event() ││ keyPressEvent() ││ paintEvent() │ └────┬─────┘└────┬────┘└────┬────┘└────┬────┘ key-event │ │ │ │ ─────→│─────────→┌┴┐───────→┌┴┐ │ paint-event │ │ │ │ │ │ ─────→│ │ │ │ │ │ : │ └┬┘ └┬┘ │ : │─────────→┌┴┐─────────│────────→┌┴┐ │ │ │ │ │ │ │ │ │ │ │ │ │ └┬┘ │ └┬┘ │ │ │ │ ↓ ↓ ↓ ↓
(A) 同一スレッドの場合 (B) 異なるスレッドの場合 ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │ Object-1 │ │ Object-2 │ │ Object-1 │ │ Object-2 │ └──┬──┘ └──┬──┘ └──┬──┘ └──┬──┘ │ │ │ │ ┌┴┐emit signal │ ┌┴┐emit signal │ │ │─────→┌┴┐ │ │──────→┌┴┐ │ │ │ │ └┬┘ │ │ │ │※この間 │ │ │※非ブロッキング│ │ │ │ブロッキング│ │ │ │ │ │ │ │ │ │ │ │ │ │←─────└┬┘ │ └┬┘ └┬┘ │ │ │ │ │ │ │ ↓ ↓ ↓ ↓
→ イベントループが回っていないスレッドに対して、非ブロッキングスロットコールを行なっても、処理されない!
QThread クラスには以下の機能・特徴がある。
ここまでを理解すると、たいていの人は以下のようにコーディングすれば良いと考えるに違いない(筆者もそうだった)。
上記方法によるコードは以下(リスト 1, 2)のようになる。
1: class MyThread : public QThread 2: { 3: Q_OBJECT 4: public: 5: MyThread(QObject *parent = 0); 6: ~MyThread(); 7: 8: signals: 9: void progress(int); // 処理進行状況通知 10: 11: public slots: 12: void doWork(); // 処理開始 13: void timeoutHandler(); // 処理中断 14: };
1: m_myThread = new MyThread; 2: connect(m_doWorkButton, SIGNAL(clicked()), m_myThread, SLOT(doWork())); 3: m_myThread->start();
が、このコードは期待したようには動作しない。
MyThread::doWork() にブレークポイントを設置し、止めてみると分かるのだが、
MyThread::doWork() は MyThread のコンテキストではなく、メインスレッドのコンテキストで実行される。
つまりマルチスレッド処理が行われないのだ。
何故そうなるかと言うと、m_myThread オブジェクトそのものはメインスレッドが所有しているからだ。
┏━メインスレッド━┓ ┏━サブスレッド━┓ ┃m_myThread ───╂─→┃ ┃ ┃mainWindow ┃ ┃ ┃ ┃..... ┃ ┃ ┃ ┗━━━━━━━━━┛ ┗━━━━━━━━┛
この問題を対処するには、QObject::moveToThread(QThread *) を使って、 m_myThread オブジェクトの所有権を m_myThread スレッドに移さなくてはいけない。
1: m_myThread = new MyThread; 2: m_myThread->moveToThread(m_myThread); // 所有権を m_myThread スレッドに移動 3: connect(m_doWorkButton, SIGNAL(clicked()), m_myThread, SLOT(doWork())); 4: m_myThread->start();
┏━メインスレッド━┓ ┏━サブスレッド━┓←─┐ ┃ ┃ ┃m_myThread ──╂──┘ ┃mainWindow ┃ ┃ ┃ ┃..... ┃ ┃ ┃ ┗━━━━━━━━━┛ ┗━━━━━━━━┛
これで、一応ちゃんと動作するのだが、なんと美しくないコードだろうか・・・
何故こんなにも残念なことになったかと言うと、QThread の派生クラスを作り、
それにシグナル・スロットを実装するという方法は QThread の推奨される使い方ではないからだ。
というわけで、QThread の推奨される使い方を示す。
コードは以下の様になる。
1: class DoSomething : public QObject 2: { 3: Q_OBJECT 4: public: 5: DoSomething(QObject *parent = 0); 6: ~DoSomething(); 7: 8: signals: 9: void progress(int); // 処理進行状況通知 10: 11: public slots: 12: void doWork(); // 処理開始 13: void timeoutHandler(); // 処理中断 14: };
1: m_doSomething = new DoSomething; 2: m_thread = new QThread; 3: m_doSomething->moveToThread(m_thread); 4: connect(m_doWorkButton, SIGNAL(clicked()), m_doSomething, SLOT(doWork())); 5: m_thread->start();
リスト 3 と比べてなんと美しいコードであろうか。
DoSomething は別スレッドで実行するかどうかにまったく依存していない。
ということは、最初は別スレッドではなくメインスレッドで実行することを前提にソースを記述し、
処理時間が長くなった場合は、リスト 5. の 2, 3 行目を追加するだけで、マルチスレッド化できるということだ。
なんと高い柔軟性であることか。素晴らしい!
※ 実際には処理をマルチスレッド化すると、排他制御やスレッド処理制御のコードが必要になることがあるので、
完全にコードを共有できるとは限らない。
┏━メインスレッド━┓ ┏━サブスレッド━┓ ┃m_thread ───╂─→┃ ┃ ┃mainWindow ┃ ┃m_doSomething ┃ ┃..... ┃ ┃ ┃ ┗━━━━━━━━━┛ ┗━━━━━━━━┛
※ 補足:別スレッドオブジェクトのスロットを直接コールすると、相手側スレッドのコンテキストではなく、
コール側スレッドのコンテキストで実行されるので注意。
相手側スレッドコンテキストで実行させたい場合は以下のように QMetaObject::invokeMethod() を使用する。
1: QMetaObject::invokeMethod(m_doSomething, "doWork"); // m_doSomething->doWork()
長時間処理を行う場合、QThread 派生クラスを定義し、そこで処理をするのではなく、
処理を行う QObject 派生クラスを定義し、moveToThread() により実行コンテキストを別スレッドに移すようにするのが本筋だぞ。