技術文章Qt

 

QThread - キュートにマルチスレッドで行こう -
Nobuhide Tsuda
21-Apr-2012

概要

基本事項

→ イベントループが回っていないスレッドに対して、非ブロッキングスロットコールを行なっても、処理されない!

QThread クラス

QThread クラスには以下の機能・特徴がある。

QThread クラスの使い方(残念な例)

ここまでを理解すると、たいていの人は以下のようにコーディングすれば良いと考えるに違いない(筆者もそうだった)。

上記方法によるコードは以下(リスト 1, 2)のようになる。

リスト 1. QThread 派生クラス定義
 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: };
リスト 2. QThread 派生クラスオブジェクト生成
 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 オブジェクトそのものはメインスレッドが所有しているからだ。

図 1. m_myThread オブジェクト自身はメインスレッド
    ┏━メインスレッド━┓    ┏━サブスレッド━┓
    ┃m_myThread  ───╂─→┃                ┃
    ┃mainWindow        ┃    ┃                ┃
    ┃.....             ┃    ┃                ┃
    ┗━━━━━━━━━┛    ┗━━━━━━━━┛

この問題を対処するには、QObject::moveToThread(QThread *) を使って、 m_myThread オブジェクトの所有権を m_myThread スレッドに移さなくてはいけない。

リスト 3. moveToThread() を使用して別スレッドに移動
 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();
図 2. m_myThread を moveToThread でサブスレッドに移動
    ┏━メインスレッド━┓    ┏━サブスレッド━┓←─┐
    ┃                  ┃    ┃m_myThread  ──╂──┘
    ┃mainWindow        ┃    ┃                ┃
    ┃.....             ┃    ┃                ┃
    ┗━━━━━━━━━┛    ┗━━━━━━━━┛

これで、一応ちゃんと動作するのだが、なんと美しくないコードだろうか・・・
何故こんなにも残念なことになったかと言うと、QThread の派生クラスを作り、 それにシグナル・スロットを実装するという方法は QThread の推奨される使い方ではないからだ。

QThread クラスの使い方(推奨される例)

というわけで、QThread の推奨される使い方を示す。

コードは以下の様になる。

リスト 4. 処理クラス定義
 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: };
リスト 5. QThread、処理クラスオブジェクト生成
 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 行目を追加するだけで、マルチスレッド化できるということだ。
なんと高い柔軟性であることか。素晴らしい!
※ 実際には処理をマルチスレッド化すると、排他制御やスレッド処理制御のコードが必要になることがあるので、 完全にコードを共有できるとは限らない。

図 2. QObject 派生クラスオブジェクトを moveToThread でサブスレッドに移動
    ┏━メインスレッド━┓    ┏━サブスレッド━┓
    ┃m_thread    ───╂─→┃                ┃
    ┃mainWindow        ┃    ┃m_doSomething   ┃
    ┃.....             ┃    ┃                ┃
    ┗━━━━━━━━━┛    ┗━━━━━━━━┛

※ 補足:別スレッドオブジェクトのスロットを直接コールすると、相手側スレッドのコンテキストではなく、 コール側スレッドのコンテキストで実行されるので注意。
相手側スレッドコンテキストで実行させたい場合は以下のように QMetaObject::invokeMethod() を使用する。

リスト 6. 処理クラススロットを直接コールしたい場合
 1:     QMetaObject::invokeMethod(m_doSomething, "doWork");  //  m_doSomething->doWork()

まとめ

長時間処理を行う場合、QThread 派生クラスを定義し、そこで処理をするのではなく、
処理を行う QObject 派生クラスを定義し、moveToThread() により実行コンテキストを別スレッドに移すようにするのが本筋だぞ。