LoginSignup
69

More than 5 years have passed since last update.

posted at

updated at

Qtでスレッドを使う前に知っておこう

はじめに

あっという間に1年が過ぎ、また今年も始まりましたQt Advent Calender。昨日は、RapidCopyのKengoSawa2さんによる「Mac App Storeで販売可能なアプリをビルドするqmakeの例と簡易解説」でした。
本日の記事は、hermit4が昨年に引き続きマルチスレッド関係にしようかと思います。

昨年は、マルチスレッド関係としては以下の2件を投稿しました。

QtConcurrentは、マルチスレッドを簡単に実現するためのハイレベルなAPI群で、同一の処理を並列に走らせるのに向いています。それに対し、QThreadはローレベルなAPIで、自分で色々と処理しなければならない反面、自由度の高いマルチスレッドが実現できます。

というあたりを駆け足でお話ししました。今回は、マルチスレッドの時に注意すべきこととか、前回書き足りなかった分について、できる範囲で補足していきたいと思います。カレンダーが空くのが残念だったので、つい3日前に登録して、いま泣きながら記事を書いている所ですので、まとまりもなく書き綴る事をお許し下さい。

QThreadの使い方の変遷

昨年の記事をお読みいただいた方で、QtのExampleやO'REILY社から出ている「入門 Qt4プログラミング」をお読みになった方は、QThreadの使い方が違うなと感じられたかと思います。

元々、QThreadは、QThreadを継承した上で、virtual void QThread::run()をオーバーライドして利用するという書き方が一般的でしたが、2011年にMayaさんがMaya Posch's blogでHow To Really, Truly Use QThreads; The Full Explanationというサブクラス化する方法ではなくもっと適切に使う方法があるのではないかとのエントリーを書いて、フォーラムに投稿されました。以後、moveToThreadを使うこの方法がより適切な使い方だとされています。

以前の書きっぷりはたとえば、Mandelbrot Exampleをご覧頂けば良いかと思います。これはGUIスレッドとフラクタル画像を生成するスレッドを分けることで、GUIを止めることなく再演算を可能とするサンプルプログラムです。

このサンプルプログラムのようなQThreadの継承を使った実装方法では、以下のような懸念点があります。

  • QThreadおよびその派生クラス(およびそのメンバ)は、そのクラスを生成したスレッドに所属する
  • QThreadおよびその派生クラス(およびそのメンバ)のスロットは、QThreadおよびその派生クラスを生成したスレッドで実行される
  • QThread::run()はデフォルトではQThread::exec()を呼んでイベントループを生成している
  • ExampleのRenderThread::run()は、exec()を呼んでいないためイベントループが無い
  • 生成されたスレッドがイベントループを持たない場合、そのスレッドは他のスレッドからのシグナルを受け付けない
  • Qtのクラスでイベントループを必要とするクラスは、そのままではRenderThread::run()の中で動作しない
  • つまり、イベントループや他スレッドからのシグナルを受け取るにはRenderThread::runでは、exec()しなくてはならない

(2016/01/05追記) : メンバ変数もまた生成スレッドに所属するため別スレッドのrun()の中から不用意にアクセスするのは危険を伴う

サンプルプログラムの場合、シグナルは発呼していますが、スロットは使っていません。またスレッドは単純な演算をするだけで、Qtのクラスはほとんど利用していないので、問題になっていないのです。

イベントループとスレッドとシグナルとスロット周りを理解するまでは、継承を使って実装すると、意図した通りに動かないと頭を悩ませるケースに遭遇します。特に、RenderThread::run()の中ではシグナルは発呼できており、普通にconnectが動作しているように見えるため、スロットを持たせると罠に落ちるわけです。

このようなわかりにくい罠を回避するには、QThread::run()をオーバーライドする方法ではなく、スレッドで動作させたいものはスロットで呼び出せるようにしたうえで、moveToThreadして、QThread::run()はそのままQThread::exec()だけ実行してイベントループを持ち、スロット経由での呼び出しをするようにしましょうというのが、現在のQThreadの使い方として認知されています。

もちろん、スレッドを使うケースでも、RenderThreadのようにスロットも、イベントループを要求するようなクラスも必要なく、計算だけをやるような場合は、無駄にイベントループが回らないように、QThread::run()を使う方が良いケースもあるかと思います。

どちらの実装方法をとるのかによって、何が違ってくるのか良く理解しながら、実装方法を検討するように心がけて下さい。

イベントループ

通常、GUIアプリケーションはイベント駆動で実装されています。つまり何らかのイベント(たとえばボタンを押す、メニューを選択する等)を待って、そのイベントに対して必要な処理を実行するというプログラムになっています。

まずは、簡単なチュートリアルをみてみましょう。

main.cpp
#include <QtWidgets>

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);
    QWidget window;
    window.resize(320, 240);
    window.setWindowTitle
          (QApplication::translate("childwidget", "Child widget"));
    window.show();

    QPushButton *button = new QPushButton(
        QApplication::translate("childwidget", "Press me"), &window);
    button->move(100, 100);
    button->show();
    return app.exec();
}

この例は、QWidgetの中にQPushButtonが1つだけあるサンプルです。これだけだと、ボタンを押しても何の動作もしません。ボタンを押すというイベントに対して、何の動作も定義されていないからです。

そこで、QPushButtonをnewした後に、以下の1行追加してみましょう。

QObject::connect(button, SIGNAL(clicked(bool)), &window, SLOT(close()));

これで実行すると、"Press me"ボタンを押した直後にウィンドウが閉じます。これは、QPushButtonをclickしたというマウスクリックのイベントに対してQPushButtonのevent処理が呼び出されてシグナル発呼し、そのシグナルに呼応してQWidgetのcloseというスロットを呼び出すようにしたためです。このボタンが押されたというイベントを待ち合わせ、押された際のイベントを処理するのがイベントループであり、上記のコードではapp.exec()がイベントループを生成しています。

このイベントループは、概念的には以下のようなものです(wikiから抜粋)。

    while (is_active) {
        while (!event_queue_is_empty)
            dispatch_next_event();

        wait_for_more_events();
    }

まずは、QCoreApplicationのソースコードを見てみましょう。

qcoreapplication.cpp
int QCoreApplication::exec()
{
    :
    QEventLoop eventLoop;
    :
    int returnCode = eventLoop.exec();
    :
    return returnCode;
}

単純にQEventLoop::exec()が呼び出されています。

qeventloop.cpp
int QEventLoop::exec(ProcessEventsFlags flags)
{
    :
    while (!d->exit.loadAcquire())
        processEvents(flags | WaitForMoreEvents | EventLoopExec);
    :
}

bool QEventLoop::processEvents(ProcessEventsFlags flags)
{
    Q_D(QEventLoop);
    if (!d->threadData->eventDispatcher.load())
        return false;
    return d->threadData->eventDispatcher.load()->processEvents(flags);
}

この先はアーキテクチャごとに変わってくるわけですが、AbstractEventDispatcherのサブクラス内で、イベントがあればそれを処理するといった形になっています。

で、重要なのが、これがQThreadData threadDataとしてスレッド単位毎に持つように実装されている事です。
Qtでは各スレッドでそれぞれにイベントのキューが存在しており、イベントを処理するためには、各スレッドでそれぞれイベントループを用意する必要があります。

threadsandobjects.png
*この画像は、Qtの公式ドキュメントのものです。

シグナルとスロット

QThreadについて昨年投稿した際は、一切説明もなしにいきなり使っていました。connectの方法で動作が変わるよという事は書きましたが、少し解説しておきます。

概要

Qtの根幹とも言うべき機能に「シグナルとスロット」があります。これは、mocという独自のプリプロセッサによりC++言語を拡張1し、QMetaObjectを利用したQObject間のメッセージ機構として提供されています2。非常に泥臭い実装部分はマクロとプリプロセッサで隠ぺいし、プログラマが必要な部分だけを少量のコードで記述できるよう工夫されています。

Qtは多くのGUIフレームワークと同様、イベント駆動型のプログラミングに向くように設計されており、GUIアプリにおいては、イベントとイベントハンドラのように利用されています。Qtのこの機構のすぐれたところは、シングルスレッド内はもちろん、実は異なるスレッド間でのメッセージ伝達にも利用できるという所です。

まずは、簡単にシグナルとスロットをどう書くのかコード例をみてみましょう。

簡単な例: Counterクラスと利用例

counter.h
#include <QObject>

class Counter : public QObject  // [A-1]
{
    Q_OBJECT  // [A-2]

public:
    Counter(int value=0, QObject* parent=0);
    int value() const { return value_; }

public slots:  // [A-3]
    void setValue(int value);

signals:  // [A-4]
    void valueChanged(int newValue);

private:
    int value_;
};
counter.cpp
#include "counter.h"

Counter::Counter(int value, QObject* parent)
       : QObject(parent), value_(value)
{
}

void Counter::setValue(int value)
{
    if (value != value_) {
        value_ = value;
        emit valueChanged(value_); // [A-5]
    }
}
main.cpp
#include <QCoreApplication>
#include "counter.h"
#include <QTextStream>

QTextStream cout(stdout);

int main(int argc, char* argv[])
{
    QCoreApplication app(argc, argv);
    Counter a(1);
    Counter b(2);
    QObject::connect(&a, SIGNAL(valueChanged(int)), &b, SLOT(setValue(int))); // [A-6]
    a.setValue(1024);
    cout << "1) a.value = " << a.value() << ", b.value = " << b.value() << endl;
    b.setValue(256);
    cout << "2) a.value = " << a.value() << ", b.value = " << b.value() << endl;
    return 0;
}

実行結果
1) a.value = 1024, b.value = 1024
2) a.value = 1024, b.value = 256

解説

  • [A-1] Qtでは、原則としてQObjectの派生クラスとすることで、シグナルとスロットが利用できます3
  • [A-2] マクロを使いメタオブジェクト等の定義等を隠ぺいしています。QObjectの派生クラスには記述するようにしましょう。
  • [A-3] 独自の拡張部分で、スロットの宣言に利用します。
  • [A-4] 独自の拡張部分で、シグナルの宣言に利用します。なお実態は自動的に実装されるため、宣言のみ記述することになります。
  • [A-5] シグナルの発呼。emitは空のマクロです。シグナルを出しているとコード上で分かりやすくするための目印です。
  • [A-6] aシグナルbスロットに届けるように登録しています。bシグナルaが受けるようには指定していないので、b.setValue(256)の結果はaには反映されていません。

このように、あるオブジェクトから通知されるシグナルをあるオブジェクトがスロットで受けることでメッセージを伝達することが可能になります。

connectの種類

さて、ざっとシグナルとスロットについて説明しましたが、「イベント駆動に向いた」と言い、「イベントとイベントハンドラのような」としながら、例題ではイベントループがない事にお気づきかもしれません。シグナルとスロットは、先ほど説明したイベントループの処理するイベント駆動の機構ではなく、オブジェクト間のメッセージ伝達の仕組みであり、利用する状況により伝達の仕方が異なります。

上記の例では、main関数内で実行が終了しているシングルスレッドのため、connectした処理は、単純にオブジェクト間のメッセージ伝達がメソッドの呼び出し、C++的にはメンバ関数の呼び出しとして処理されています。つまり、aオブジェクト内のsetValueにおけるemit valueChanged(value_);が、(&b)->setValue(value_);の関数呼び出しへと置き換わったと考えると良いでしょう。

上記の例は単純なシングルスレッドの例でしたので、スロットが即時呼び出されましたが、マルチスレッドの場合、異なる動作が平行または並列に動作しており、一方から他方をいきなり呼び出すと、予期せぬタイミングでの呼び出しなりかねません。そこで、Qtでは、シグナルの発呼側と受信するスロット側のスレッドが異なる場合、デフォルトでは、シグナルが一度キューにいれられ、受信側のスレッドのイベントループで処理されるように実装されています。

この動作は、QObject::connecttype引数で指定できるようになっています。


QMetaObject::Connection 
QObject::connect(const QObject * sender, 
                 PointerToMemberFunction signal, 
                 const QObject * receiver, 
                 PointerToMemberFunction method, 
                 Qt::ConnectionType type = Qt::AutoConnection)

上記の動作の他に、異なるスレッド間でスロット実行終了を待ち合わせるという設定も可能です。以下がQt::ConnectionTypeで呼び出し方法の種類となります。

  • Qt::AutoConnection
    受信側が同じスレッド上にいる場合は DirectoConnection となり、それ以外の場合は、 QueuedConnection となる。

  • Qt::DirectConnection
    スロットの直接呼出し。シグナルを発呼したスレッドから対象のスロットを呼び出します

  • Qt::QueuedConnection
    スロットのイベントループ呼び出し。
    シグナルは受信側のスレッドのキューに入れられて、受信側スレッドのイベントループによりスロットが呼び出されます。
    シグナル発呼側は、レシーバー側のキューに入れて即時処理を再開します(スロットの実行を待たない)

  • Qt::BlockingQueuedConnection
    スロットのイベントループ呼び出し。
    シグナルは受信側のスレッドのキューに入れられて、受信側スレッドのイベントループによりスロットが呼び出されます。
    シグナル発呼側は、受信側のスロット実行終了を待ち合わせます。

覚えておいた方が良いのは、

  • シグナルイベントは別物。スロットの処理の一部がイベントループで行われる場合があるだけ
  • DirectConnectionの場合、発呼側スレッド上でスロット呼び出しが行われる
  • QueuedConnection/BlockingQueuedConnectionの場合、受信側スレッドでスロット呼び出しが行われる
  • QueuedConnection/BlockingQueuedConnectionの場合、受信側スレッドにイベントループが必要
  • QueuedConnection/BlockingQueuedConnectionの場合、受信側スレッドがイベントループに戻るまで遅延される
  • BlockingQueuedConnectionの場合、待ち合わせが発生するため、設計を誤るとデッドロックを起こす

という事になります。マルチスレッドと合わせてご利用になる際は、このあたりに注意すると深みにはまることなく進めるかと思います。

ところで、このシグナルとスロットの機能のため、QObjectとそのサブクラスは、いずれかのスレッドのイベントループに登録されます。このとき、親子関係を持つQObjectは、必ず同一のスレッドに登録される必要があります。つまり、移したいQObjectはその親ごとmoveToThreadしなくてはならない事に注意が必要です。

まとめ

すいません。ネタもろくに考える余裕もなく書きつけたので、雑然としていてすいません。

今回は

  • 前回書いたQThreadの使い方とExamples/Qt本が違う理由
  • イベントループ
  • シグナルとスロットのスレッドでの挙動

について、かなり端折って書きつけてみました。ご要望や質問があれば、随時加筆修正していきますので、コメントを残して下さい。

Advent Calendar 2015の明日3日目は、informationseaさんによる「OS Xでメニューを適切な場所に表示する」というノウハウのようです。Mac向けはメンテナも少なく情報もなかなか見つからないので、1日目といい3日目といい、Macユーザーに良い情報が出てきてくれてうれしいです。

まだまだカレンダーの予定が空いているので、一晩で書ける適当な量でかまいません。ぜひ、Qtを使って何かのノウハウや、書きつけておきたいことがある方は、どしどし登録してください。

注釈


  1. このため、C++の規格にうるさい方には不評な場合があるようですが、元々C++自体がC言語を独自のプリプロセッサで拡張する形で誕生したのは興味深い歴史です 

  2. boostにもC++の規格に則って作られたシグナルとスロット機構が用意されましたが、Qtとの共存についてはバージョンにより色々と注意が必要なケースがあります 

  3. 例外もありますが、ここでは省略します。気になる方はQMetaObject関連を調べてみて下さい 

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
69