Taro3

View on GitHub

QRunnableでジョブクラスを定義する

プロジェクトの核心部分に飛び込んでみましょう。マンデルブロ画像生成を高速化するために、計算全体を複数のジョブに分割します。Jobとは、タスクのリクエストです。CPUアーキテクチャにもよりますが、複数のジョブが同時に実行されます。Jobクラスは、結果値を含むJobResult関数を生成します。このプロジェクトでは、Jobクラスは、画像全体の1行分の値を生成します。例えば、800 x 600 の画像解像度の場合、600 個のジョブが必要で、それぞれが 800 個の値を生成します。

JobResult.hというC++ヘッダーファイルを作成してください。

#include <QSize>
#include <QVector>
#include <QPointF>

struct JobResult
{
    JobResult(int valueCount = 1) :
    areaSize(0, 0),
    pixelPositionY(0),
    moveOffset(0, 0),
    scaleFactor(0.0),
    values(valueCount)
    {
    }

    QSize areaSize;
    int pixelPositionY;
    QPointF moveOffset;
    double scaleFactor;

    QVector<int> values;
};

この構造体には 2 つの部分があります。

これで Job クラス自体を作成することができます。次のJob.hのスニペットを内容に使用して、C++クラスJobを作成します。

#include <QObject>
#include <QRunnable>

#include "JobResult.h"

class Job : public QObject, public QRunnable
{
    Q_OBJECT

public:
    Job(QObject *parent = 0);
    void run() override;
};

このJobクラスはQRunnableなので、run()をオーバーライドしてマンデルブロ・ピクチャーアルゴリズムを実装することができます。ご覧のように、Job は QObject を継承しているので、Qt の signal/slot 機能を使用することができます。アルゴリズムはいくつかの入力データを必要とします。Job.h をこのように更新してください。

#include <QObject>
#include <QRunnable>
#include <QPointF>
#include <QSize>
#include <QAtomicInteger>

class Job : public QObject, public QRunnable
{
    Q_OBJECT

public:
    Job(QObject *parent = 0);
    void run() override;
    void setPixelPositionY(int value);
    void setMoveOffset(const QPointF& value);
    void setScaleFactor(double value);
    void setAreaSize(const QSize& value);
    void setIterationMax(int value);

private:
    int mPixelPositionY;
    QPointF mMoveOffset;
    double mScaleFactor;
    QSize mAreaSize;
    int mIterationMax;
};

これらの変数の話をしましょう。

これで、Job.hにシグナル、jobCompleted()、アボート機能を追加することができます。

#include <QObject>
#include <QRunnable>
#include <QPointF>
#include <QSize>
#include <QAtomicInteger>

#include "JobResult.h"

class Job : public QObject, public QRunnable
{
    Q_OBJECT

public:
    ...

signals:
    void jobCompleted(JobResult jobResult);

public slots:
    void abort();

private:
    QAtomicInteger<bool> mAbort;
    ...
};

アルゴリズムが終了すると、jobCompleted() シグナルが出力されます。jobResult パラメータには結果値が格納されます。abort() スロットは、mIsAbort フラグの値を更新しているジョブを停止させることができます。mAbortは古典的なboolではなく、QAtomicInteger<bool>であることに注意してください。この Qt のクロスプラットフォーム型により、アトミックな操作を中断することなく実行することができます。mutex や他の同期機構を使用してもよいのですが、アトミック変数を使用することで、異なるスレッドから安全に変数を更新したりアクセスしたりするための高速な方法です。

Job.cppで実装部分に切り替えましょう。ここに Job クラスのコンストラクタがあります。

#include "Job.h"

Job::Job(QObject* parent) :
    QObject(parent),
    mAbort(false),
    mPixelPositionY(0),
    mMoveOffset(0.0, 0.0),
    mScaleFactor(0.0),
    mAreaSize(0, 0),
    mIterationMax(1)
{
}

これは古典的な初期化です。QObjectのコンストラクタを呼び出すことを忘れないでください。

これで、run()関数を実装できるようになりました。

void Job::run()
{
    JobResult jobResult(mAreaSize.width());
    jobResult.areaSize = mAreaSize;
    jobResult.pixelPositionY = mPixelPositionY;
    jobResult.moveOffset = mMoveOffset;
    jobResult.scaleFactor = mScaleFactor;
    ...
}

この最初の部分では、JobResultの変数を初期化します。領域サイズの幅を利用して、JobResult::valueを正しい初期サイズのQVectorとして構築します。その他の入力データは、JobからJobResultにコピーして、JobResultの受信側にコンテキスト入力データで結果を取得させるようにしています。

そして、run()関数をマンデルブロアルゴリズムで更新することができます。

void Job::run()
{
    ...
    double imageHalfWidth = mAreaSize.width() / 2.0;
    double imageHalfHeight = mAreaSize.height() / 2.0;
    for (int imageX = 0; imageX < mAreaSize.width(); ++imageX) {
        int iteration = 0;
        double x0 = (imageX - imageHalfWidth)
            * mScaleFactor + mMoveOffset.x();
        double y0 = (mPixelPositionY - imageHalfHeight)
            * mScaleFactor - mMoveOffset.y();
        double x = 0.0;
        double y = 0.0;
        do {
            if (mAbort.load()) {
                return;
            }

            double nextX = (x * x) - (y * y) + x0;
            y = 2.0 * x * y + y0;
            x = nextX;
            iteration++;

        } while(iteration < mIterationMax
            && (x * x) + (y * y) < 4.0);

        jobResult.values[imageX] = iteration;
    }

    emit jobCompleted(jobResult);
}

マンデルブロアルゴリズム自体は本書の範囲を超えています。しかし、このrun()関数の主な目的を理解する必要があります。それを分解してみましょう。

最後の関数はabort()スロットです。

void Job::abort()
{
    mAbort.store(true);
}

このメソッドは、値 true のアトミック書き込みを実行します。アトミックなメカニズムにより、run() 関数での mAbort の読み込みを中断することなく、複数のスレッドから abort() を呼び出すことができます。

この場合、run()はQThreadPoolの影響を受けるスレッド内に存在します(これについては近日中に説明します)が、abort()スロットはMandelbrotCalculatorのスレッドコンテキスト内で呼び出されます。

mAbort上の操作をQMutexで確保した方がいいかもしれません。しかし、ミューテックスのロックとアンロックを頻繁に行うと、コストのかかる操作になることを覚えておいてください。ここでQAtomicIntegerクラスを使用することで、利点だけが得られます。mAbortへのアクセスはスレッドセーフであり、高価なロックを避けることができます。

Jobの実装の最後にはセッター関数のみが含まれています。疑問がある場合は、完全なソースコードを参照してください。


戻る