QThreadを使ってみよう

  • 26
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

QThread入門

昨日、真面目な記事を書いたら、Twitterで @hermit4 が真面目な記事を書くなんて!とツッコミを受けた悲しい気分のhermit4です。真面目で誠実が売りの寡黙な人なのですけどねぇ(この件に関してはツッコミ禁止)。

まぁ、気を取り直して、本日4日目は、昨日に引き続き、マルチスレッドを続けてみましょう。

マルチスレッドは、複数のスレッド間でデータ領域や一部のリソースが共有されてしまうため、実際は色々難しい局面が発生します。罠にはまる事もあるので、注意事項とか、同期処理とか難しい所を題材にしようかと思いましたが、クリスマスに向けて、楽しい気分?のこの時期ですから、ネガティブな話はおいといて、何となく出来そうな気がするというエンターテイメント的入門編で進めてみようかなと思っています。

何にせよプログラムなんてものは、まずはコードを書いてみる事が重要です。マルチスレッドはちょっと・・・と敬遠しているような人とか、難しそうと尻込みしてそうな人が、なんか出来そう・・・とでも思って手を出してみるきっかけにでもなれば幸いです。

QtConcurrentとQThreadの使い分け

先日の記事に書いたQtConcurrentは、マルチスレッドを簡単に実現するためのハイレベルなAPIです。同じ事を行うスレッドを複数生成し、それぞれに処理させるのに適しているのに対し、それぞれが違う役割をもった複数のスレッドを連携させるような用途には向いていません。

そんな場合は、ローレベルなQThreadクラスを使ってマルチスレッドを実現して行く事になります。

題材

何か面白い事を・・・と思ったのですが、複雑なコードになるとそれを追うだけで面倒になるかもしれません。というわけで、以下の処理を考えてみましょう。

  1. サイコロを10回振る
  2. 1の結果を受け取り平均値を算出する
  3. 1の結果を受け取り中央値を探す
  4. 1の結果を受け取って最頻値を探す

この2,3,4の演算を別々のスレッドで行わせるという事をやってみましょう。(そんなのスレッドにするほどの計算でもないじゃんという意見はおいておいて・・・)

まずはシングルスレッドで実装してみる

シングルスレッドでは無駄に思えるかもしれませんけど、並列で動かしたい処理は、それぞれ異なるQObjectを継承したクラスとして実装します。

  • ランダムなダイス値のリストを生成するクラス
randomlistgenerator.h
#pragma once
#include <QObject>

class RandomListGenerator : public QObject
{
    Q_OBJECT
public:
    explicit RandomListGenerator(int num, QObject *parent = 0);

signals:
    void generated(const QList<int>& data);

public slots:
    QList<int> generate();

private:
    int num_;
};

randomlistgenerator.cpp
#include "randomlistgenerator.h"
#include <QDateTime>

RandomListGenerator::RandomListGenerator(int num, QObject *parent) :
    QObject(parent), num_(num)
{
    qsrand(QDateTime::currentMSecsSinceEpoch()/1000);
}

QList<int> RandomListGenerator::generate()
{
    QList<int> list;
    for (int i=0; i<num_ ; ++i) {
        list << qrand() % 6 + 1;
    }
    emit generated(list);
    return list;
}
  • 平均値を計算するクラス
calcaverage.h
#pragma once

#include <QObject>

class CalcAverage : public QObject
{
    Q_OBJECT
public:
    explicit CalcAverage(QObject *parent = 0);

public slots:
    void calc(const QList<int>& data);
    double getAverage() const { return average_; }

private:
    double average_;
};

calcaverage.cpp
#include "calcaverage.h"
#include <QThread>
#include <QDebug>

CalcAverage::CalcAverage(QObject *parent) :
    QObject(parent), average_(qQNaN())
{
}

void CalcAverage::calc(const QList<int> &data)
{
    qDebug() << "CalcAverage::calc " << QThread::currentThread();
    if (data.isEmpty()) {
        average_ = qQNaN();
        return;
    }
    int sum = 0;
    foreach (int i, data) {
        sum += i;
    }
    average_ = static_cast<double>(sum)/data.size();
}
  • 中央値を計算するクラス
calcmedian.h
#pragma once
#include <QObject>

class CalcMedian : public QObject
{
    Q_OBJECT
public:
    explicit CalcMedian(QObject *parent = 0);

public slots:
    void calc(const QList<int>& data);
    double getMedian() const { return median_; }

private:
    double median_;
};
calcmedian.cpp
#include "calcmedian.h"
#include <QtMath>
#include <QThread>
#include <QDebug>

CalcMedian::CalcMedian(QObject *parent) :
    QObject(parent), median_(qQNaN())
{
}

void CalcMedian::calc(const QList<int> &data)
{
    qDebug() << "CalcMedian:::calc " << QThread::currentThread();
    if (data.isEmpty()) {
        median_ = qQNaN();
    } else {
        QList<int> tmp(data);
        qSort(tmp);
        if (data.size() == 1) {
            median_ = data.at(0);
        } else if (data.size() % 2) {
            median_ = data.at(data.size() % 2);
        } else {
            int a = qCeil(static_cast<double>(data.size()-1)/2);
            int b = qFloor(static_cast<double>(data.size()-1)/2);
            median_ = static_cast<double>(data.at(a)+data.at(b))/2.0;
        }
    }
}
  • 最頻値を計算するクラス
calcmode.h
#pragma once

#include <QObject>

class CalcMode : public QObject
{
    Q_OBJECT
public:
    explicit CalcMode(QObject *parent = 0);

public slots:
    void calc(const QList<int>& data);
    QList<int> getMode() const { return mode_; }

private:
    QList<int> mode_;
};

calcmode.cpp
#include "calcmode.h"
#include <QMap>
#include <QThread>
#include <QDebug>

CalcMode::CalcMode(QObject *parent) :
    QObject(parent)
{
}

void CalcMode::calc(const QList<int> &data)
{
    qDebug() << "CalcMode::calc " << QThread::currentThread();
    QMap<int, int> counts;
    int max = 0;
    foreach (int i, data) {
        if (counts.contains(i)) {
            counts[i]++;
        } else {
            counts[i] = 0;
        }
        max = qMax(max, counts[i]);
    }
    mode_.clear();
    QMapIterator<int,int> itr(counts);
    while (itr.hasNext()) {
        if (itr.next().value() == max) {
            mode_ << itr.key();
        }
    }
}
  • 生成クラスと計算クラスの関連付けるクラス
diceplay.h
#pragma once

#include <QObject>

class RandomListGenerator;
class CalcAverage;
class CalcMedian;
class CalcMode;

class DicePlay: public QObject
{
    Q_OBJECT

public:
    DicePlay(int num, QObject* parent=0);

public slots:
    void play();

signals:
    double     getAverage();
    double     getMedian();
    QList<int> getMode();
    void fin();

private:
    RandomListGenerator* generator_;
    CalcAverage*         calcAverage_;
    CalcMedian*          calcMedian_;
    CalcMode*            calcMode_;
};
diceplay.cpp
#include "diceplay.h"
#include "randomlistgenerator.h"
#include "calcaverage.h"
#include "calcmedian.h"
#include "calcmode.h"
#include <QCoreApplication>
#include <QThread>
#include <QDebug>

DicePlay::DicePlay(int num, QObject *parent)
    : QObject(parent),
      generator_(new RandomListGenerator(num, this)),
      calcAverage_(new CalcAverage(this)),
      calcMedian_(new CalcMedian(this)),
      calcMode_(new CalcMode(this))
{
    connect(generator_, SIGNAL(generated(QList<int>)), calcAverage_, SLOT(calc(QList<int>)));
    connect(generator_, SIGNAL(generated(QList<int>)), calcMedian_, SLOT(calc(QList<int>)));
    connect(generator_, SIGNAL(generated(QList<int>)), calcMode_, SLOT(calc(QList<int>)));
    connect(this, SIGNAL(getAverage()), calcAverage_, SLOT(getAverage()));
    connect(this, SIGNAL(getMedian()), calcMedian_, SLOT(getMedian()));
    connect(this, SIGNAL(getMode()), calcMode_, SLOT(getMode()));
}

void DicePlay::play()
{
    qDebug() << "DicePlay::play " << QThread::currentThread();
    foreach(int i, generator_->generate()) {
        qDebug() << i;
    }
    qDebug() << "average : " << getAverage();
    qDebug() << "median  : " << getMedian();
    qDebug() << "mode    : " << getMode();
    emit fin();
}
  • main関数
main.cpp
#include <QCoreApplication>
#include <QMetaObject>
#include "diceplay.h"

int main(int argc, char *argv[])
{
    qRegisterMetaType<QList<int> >("QList<int>");
    QCoreApplication app(argc, argv);
    DicePlay dice(10);
    QObject::connect(&dice, SIGNAL(fin()), &app, SLOT(quit()));
    QMetaObject::invokeMethod(&dice, "play", Qt::QueuedConnection);
    return app.exec();
}

解説

今回もGUIアプリケーションではなく、コンソールアプリケーションにしています。UIのデザインをしてとか、そういう余計な手間を減らせるようにという事で。

QCoreApplication::exec()が実行されるとイベントループが開始します。ところで、Qtでコンソールアプリケーションを作り、イベントループが必要なコードを書いたとき、最初のイベントをどうするのか迷った事はないでしょうか。

上記のmainではQMetaObjectを使って、DicePlayクラスのスロット"play"の呼び出しをキューに登録しています。app.exec()の呼び出しでイベントループが始まるとキューが処理されて、DicePlay::play()が呼び出されます。

play()の中では、RandomListGenerator::generate()を実行し、QListが生成され、generated()シグナルが発行されます。
そうすると、generatedシグナルとconnectされた、CalcAverage::calc, CalcMedian::calc, CalcMode::calcのスロットが呼び出され計算が行われます。

その後、play()は、計算結果をそれぞれから取得し、qDebugに表示してfin()シグナルを発呼して終了します。
fin()シグナルは、QCoreApplicationのquitスロットとconnectされているため、そのままイベントループが終了して、プログラムは終了するという流れです。

これらは、すべて1つのスレッド上で実行されます。

DicePlay::play  QThread(0x3fbeb0)
RandomListGenerator::generate  QThread(0x3fbeb0)
CalcAverage::calc  QThread(0x3fbeb0)
CalcMedian:::calc  QThread(0x3fbeb0)
CalcMode::calc  QThread(0x3fbeb0)
3
1
6
3
6
5
4
2
1
6
average :  3.7
median  :  5.5
mode    :  (6)
<

マルチスレッド化する

さて、ではこのコードをマルチスレッド化してみましょう。修正するのは、diceplay.hとdiceplay.cppの2つのファイルとなります。

diceplay.h
#pragma once

#include <QObject>

class RandomListGenerator;
class CalcAverage;
class CalcMedian;
class CalcMode;
class QThread;

class DicePlay: public QObject
{
    Q_OBJECT

public:
    DicePlay(int num, QObject* parent=0);
    ~DicePlay();

public slots:
    void play();

signals:
    double     getAverage();
    double     getMedian();
    QList<int> getMode();
    void fin();

private:
    RandomListGenerator* generator_;
    CalcAverage*         calcAverage_;
    CalcMedian*          calcMedian_;
    CalcMode*            calcMode_;
    QThread*             averageThread_;
    QThread*             medianThread_;
    QThread*             modeThread_;
};

まず、ヘッダの方では以下を追加しています

  • デストラクタ定義
  • QThreadクラス用のポインタを3スレッド分
calcmedian.cpp
#include "diceplay.h"
#include "randomlistgenerator.h"
#include "calcaverage.h"
#include "calcmedian.h"
#include "calcmode.h"
#include <QCoreApplication>
#include <QThread>
#include <QDebug>

DicePlay::DicePlay(int num, QObject *parent)
    : QObject(parent),
      generator_(new RandomListGenerator(num, this)),
      calcAverage_(new CalcAverage),
      calcMedian_(new CalcMedian),
      calcMode_(new CalcMode),
      averageThread_(new QThread(this)),
      medianThread_(new QThread(this)),
      modeThread_(new QThread(this))
{
    calcAverage_->moveToThread(averageThread_);
    calcMedian_->moveToThread(medianThread_);
    calcMode_->moveToThread(modeThread_);

    connect(averageThread_, SIGNAL(finished()), calcAverage_, SLOT(deleteLater()));
    connect(medianThread_, SIGNAL(finished()), calcMedian_, SLOT(deleteLater()));
    connect(modeThread_, SIGNAL(finished()), calcMode_, SLOT(deleteLater()));

    connect(generator_, SIGNAL(generated(QList<int>)), calcAverage_, SLOT(calc(QList<int>)));
    connect(generator_, SIGNAL(generated(QList<int>)), calcMedian_, SLOT(calc(QList<int>)));
    connect(generator_, SIGNAL(generated(QList<int>)), calcMode_, SLOT(calc(QList<int>)));
    connect(this, SIGNAL(getAverage()), calcAverage_, SLOT(getAverage()), Qt::BlockingQueuedConnection);
    connect(this, SIGNAL(getMedian()), calcMedian_, SLOT(getMedian()), Qt::BlockingQueuedConnection);
    connect(this, SIGNAL(getMode()), calcMode_, SLOT(getMode()), Qt::BlockingQueuedConnection);

    averageThread_->start();
    medianThread_->start();
    modeThread_->start();
}

DicePlay::~DicePlay()
{
    averageThread_->exit();
    medianThread_->exit();
    modeThread_->exit();
    averageThread_->wait();
    medianThread_->wait();
    modeThread_->wait();
}

void DicePlay::play()
{
    qDebug() << "DicePlay::play " << QThread::currentThread();
    foreach(int i, generator_->generate()) {
        qDebug() << i;
    }
    qDebug() << "average : " << getAverage();
    qDebug() << "median  : " << getMedian();
    qDebug() << "mode    : " << getMode();
    emit fin();
}

Body側では、まずCalcAverage,CalcMedian,CalcModeの3つのクラスの親はなしに変更しています。その後、それぞれに割り当てるスレッドの管理クラスQThreadを生成、calcAverage_はaverageThread_へ、calcMedian_はmedianThread_へ、calcMode_はmodeThread_へとイベントループの所属を変更しています。

      calcAverage_(new CalcAverage),
      calcMedian_(new CalcMedian),
      calcMode_(new CalcMode),
      averageThread_(new QThread(this)),
      medianThread_(new QThread(this)),
      modeThread_(new QThread(this))
{
    calcAverage_->moveToThread(averageThread_);
    calcMedian_->moveToThread(medianThread_);
    calcMode_->moveToThread(modeThread_);

    connect(averageThread_, SIGNAL(finished()), calcAverage_, SLOT(deleteLater()));
    connect(medianThread_, SIGNAL(finished()), calcMedian_, SLOT(deleteLater()));
    connect(modeThread_, SIGNAL(finished()), calcMode_, SLOT(deleteLater()));

Qtはイベント駆動型のフレームワークで、通常イベントループと呼ぶループでイベントを待ち合わせており、イベントが届くと何らかの処理を起動するという動作を行っています。たとえばマウスを動かすとマウスが移動したというイベントが発生して、画面上のカーソルの描画位置を変更する処理が呼び出されるというような動作になります。

プログラムを起動した際には、mainはmain threadと呼ぶ1つだけのスレッドで動作していますが、QThread::startでスレッドを開始すると、各スレッド毎に各々イベントループが用意されます(注:Qt3くらいの頃は自分でQThreadを派生してrunをカスタマイズする必要があった気がしますが、現在はQThreadそのままでイベントループが動作します)。QObjectは、いずれかのスレッドのイベントループに所属することになりますが、moveToThreadはその所属スレッドを変更するためのメソッドです。

なお、QObjectは親が指定されている場合、親の所属するスレッドに所属することになり、moveToThreadでスレッドを移動できません。そのため、上述の通り、親の指定をはずしています。なお、親がいないQObjectの派生クラスをnewした場合は、自動的にdeleteされないため、それぞれのスレッドが終了するときに発呼されるfinished()シグナルとdeleteLaterをconnectして、スレッドが終了すると削除されるように設定しています。


    connect(this, SIGNAL(getAverage()), calcAverage_, SLOT(getAverage()), Qt::BlockingQueuedConnection);
    connect(this, SIGNAL(getMedian()), calcMedian_, SLOT(getMedian()), Qt::BlockingQueuedConnection);
    connect(this, SIGNAL(getMode()), calcMode_, SLOT(getMode()), Qt::BlockingQueuedConnection);

次に、それぞれのCalc系クラスから計算結果を取得していたシグナルとスロットの接続を、BlockingQueuedConnection方式に変更しています。

シグナルとスロットは、DirectConnectionといってシグナルが発呼された時点で直接スロットを呼び出す接続方式と、QueuedConnectionといって、イベントキューに登録しておき、イベントループでイベントキューが処理されてスロットが呼び出される方式とがあります。

通常、接続方式を指定していない場合、AutoConnectionという接続になっており、同じスレッド内のQObject派生クラスどうしではDirectConnectionに、異なるスレッドに所属するQObject派生クラスどうしではQueuedConnectionになります。

通常、QueuedConnectionは呼び出しがキューに入れられるとシグナルの発呼が終了して次の処理に移るのですが、結果を取得するような場合、キューが処理され実際にスロットの動作を待たなくてはなりません。このような場合に、BlockingQueuedConnectionを利用すると、スロットが呼び出されて処理が終了するまでシグナルを発呼したスレッドは待ち状態になります。

    averageThread_->start();
    medianThread_->start();
    modeThread_->start();

良くある凡ミスとしては、最後にスレッドをstartすることを忘れてたりします。QThread::startを呼び出さないとスレッドが動き始めないため、connect等をしっかりしたのに、何も起きない・・・という事になります。忘れずにstartさせて下さい。

    averageThread_->exit();
    medianThread_->exit();
    modeThread_->exit();
    averageThread_->wait();
    medianThread_->wait();
    modeThread_->wait();

最後に、DicePlayは自身がデストラクトされる際、startしたスレッドのイベントループを終了し、スレッドが終わるのを待ち合わせています。

このコードを実行すると、DicePlay::playとRandomListGenerator::generateはそのままmain threadで動作し、Calc系はそれぞれ別のスレッドで計算した後、結果の取得をしていることがわかります。

DicePlay::play  QThread(0x1dbf50)
RandomListGenerator::generate  QThread(0x1dbf50)
2
CalcAverage::calc  QThread(0x1dc310)
CalcMedian:::calc  QThread(0x1dc360)
CalcMode::calc  QThread(0x1dc3b0)
1
4
2
5
1
1
6
4
1
average :  2.7
median  :  3
mode    :  (1)
<

ご覧のとおり、負荷が高くなるであろう処理は、異なるQObjectの派生クラスのスロットとして実装してあれば、割と簡単に別スレッドとして動作させられることが見てとれたかと思います。まぁ、実際のところ、この程度の軽い処理ですと、マルチスレッドにした方が処理時間は遅くなっているかと思います(スレッドとして動作させるためのコストがかかってますので)。

真面目にスレッドを使い始めると、QMutexでの排他処理やQReadWriteLock, QReadLocker, QWriteLockerといったRead-Write Lock用のクラス、QThreadStorageのようなスレッド毎のデータストレージ等を利用したり、QWaitConditionでの待ち合わせ等、必要になる知識も色々ありますが、まず、てっとり早くのお気楽マルチスレッド化としては、こんなところで十分でしょう・・・・たぶん。

他にもQtConcurrentとQThreadの間くらいになるかもしれませんが、スレッドのコレクションを管理する、QThreadPoolとQRunnableといった実現方法もあるのですが、二日間分のカレンダーで話しきれなかったこれらの事は、おいおいどこかで書いていければなぁと思います。

なお、正規のドキュメントへのリンクが足りないとか、参考文献とかあれば書いていた方がといった意見をちらほら聞いていますので、昨日の記事と合わせて、この週末に少しアップデートする予定です。気になる点、誤字、脱字、日本語が変、お前が変といったご意見、ご感想があれば、どしどしコメントお残し下さいませ。

それでは、次回は、僕も書きたいのでカレンダーあけて下さい!という人がいなければ、12月11日にライセンス周りについてちょっと調べて記事を上げさせていただきます。

明日は、 @task_jp さんによる「何か」の記事となりますので、何になるのか楽しみにお待ちしましょう。

この投稿は Qt Advent Calendar 20144日目の記事です。