Taro3

View on GitHub

MandelbrotCalculatorでのQThreadPoolの使用

これで、Jobクラスの準備ができましたので、ジョブを管理するクラスを作成する必要があります。新しいクラス、MandelbrotCalculatorを作成してください。MandelbrotCalculator.hというファイルの中に必要なものを見てみましょう。

#include <QObject>
#include <QSize>
#include <QPointF>
#include <QElapsedTimer>
#include <QList>

#include "JobResult.h"

class Job;
class MandelbrotCalculator : public QObject
{
    Q_OBJECT

public:
    explicit MandelbrotCalculator(QObject *parent = 0);
    void init(QSize imageSize);

private:
    QPointF mMoveOffset;
    double mScaleFactor;
    QSize mAreaSize;
    int mIterationMax;
    int mReceivedJobResults;
    QList<JobResult> mJobResults;
    QElapsedTimer mTimer;
};

前のセクションでは、すでに mMoveOffset、mScaleFactor、mAreaSize、および mIterationMax について説明しました。また、いくつかの新しい変数もあります。

これで、すべてのメンバ変数のイメージがつかめたので、シグナル、スロット、プライベートメソッドを追加します。MandelbrotCalculator.hファイルを更新してください。

...
class MandelbrotCalculator : public QObject
{
    Q_OBJECT

public:
    explicit MandelbrotCalculator(QObject *parent = 0);
    void init(QSize imageSize);

signals:
    void pictureLinesGenerated(QList<JobResult> jobResults);
    void abortAllJobs();

public slots:
    void generatePicture(QSize areaSize, QPointF moveOffset,
    double scaleFactor, int iterationMax);
    void process(JobResult jobResult);

private:
    Job* createJob(int pixelPositionY);
    void clearJobs();

private:
    ...
};

これらの役割をご紹介します。

ヘッダーファイルが完成したので、実装を行います。ここからがMandelbrotCalculator.cppの実装の始まりです。

#include <QDebug>
#include <QThreadPool>

#include "Job.h"

const int JOB_RESULT_THRESHOLD = 10;

MandelbrotCalculator::MandelbrotCalculator(QObject *parent)
    : QObject(parent),
    mMoveOffset(0.0, 0.0),
    mScaleFactor(0.005),
    mAreaSize(0, 0),
    mIterationMax(10),
    mReceivedJobResults(0),
    mJobResults(),
    mTimer()
{
}

いつものように、メンバー変数のデフォルト値を持つイニシャライザリストを使用しています。JOB_RESULT_THRESHOLDの役割については、すぐに説明します。ここでは、generatePicture()スロットを使用しています。

void MandelbrotCalculator::generatePicture(QSize areaSize, QPointF
    moveOffset, double scaleFactor, int iterationMax)
{
    if (areaSize.isEmpty()) {
        return;
    }

    mTimer.start();
    clearJobs();

    mAreaSize = areaSize;
    mMoveOffset = moveOffset;
    mScaleFactor = scaleFactor;
    mIterationMax = iterationMax;

    for(int pixelPositionY = 0;
        pixelPositionY < mAreaSize.height(); pixelPositionY++) {
        QThreadPool::globalInstance()->
            start(createJob(pixelPositionY));
    }
}

areaSize次元が0x0であれば、何もすることはありません。リクエストが有効であれば、mTimerを起動して生成期間全体を追跡することができます。新しい画像生成のたびに、まず clearJobs() を呼び出して既存のジョブをキャンセルします。次に、提供されたものでメンバー変数を設定します。最後に、各垂直方向のピクチャラインに新しいJobクラスを作成します。Job*値を返すcreateJob()関数については、すぐに説明します。

QThreadPool::globalInstance()は、CPUのコア数に応じて最適なグローバルスレッドプールを提供してくれる静的関数です。すべてのジョブクラスに対して start() を呼び出しても、最初のクラスだけがすぐに起動します。他のクラスは、利用可能なスレッドを待ってプールキューに追加されます。

では、createJob() 関数を使って Job クラスがどのように作成されるか見てみましょう。

Job* MandelbrotCalculator::createJob(int pixelPositionY)
{
    Job* job = new Job();

    job->setPixelPositionY(pixelPositionY);
    job->setMoveOffset(mMoveOffset);
    job->setScaleFactor(mScaleFactor);
    job->setAreaSize(mAreaSize);
    job->setIterationMax(mIterationMax);

    connect(this, &MandelbrotCalculator::abortAllJobs,
        job, &Job::abort);

    connect(job, &Job::jobCompleted,
        this, &MandelbrotCalculator::process);

    return job;
}

ご覧のように、ジョブはヒープ上に割り当てられています。この操作は、MandelbrotCalculatorスレッドで時間がかかります。しかし、結果はそれに見合うだけの価値があります。QThreadPool::start()を呼び出すと、スレッドプールがjobの所有権を取得することに注意してください。その結果、Job::run()が終了すると、スレッドプールによって削除されます。マンデルブロアルゴリズムで必要とされるJobクラスの入力データを設定します。

その後、2つのコネクションが実行されます。

最後に、 Jobポインターが呼び出し元(この例では、generatePicture()スロット)に返されます。

最後のヘルパー関数は clearJobs() です。これをMandelbrotCalculator.cppに追加してください。

void MandelbrotCalculator::clearJobs()
{
    mReceivedJobResults = 0;
    emit abortAllJobs();
    QThreadPool::globalInstance()->clear();
}

受信したジョブ結果のカウンタがリセットされます。アクティブなジョブをすべて中止するためのシグナルを発します。最後に、スレッドプールで利用可能なスレッドを待っているキューに入っているジョブを削除します。

このクラスの最後の関数はprocess()で、おそらく最も重要な関数です。以下のスニペットでコードを更新してください。

void MandelbrotCalculator::process(JobResult jobResult)
{
    if (jobResult.areaSize != mAreaSize ||
            jobResult.moveOffset != mMoveOffset ||
            jobResult.scaleFactor != mScaleFactor) {
        return;
    }

    mReceivedJobResults++;
    mJobResults.append(jobResult);

    if (mJobResults.size() >= JOB_RESULT_THRESHOLD ||
            mReceivedJobResults == mAreaSize.height()) {
        emit pictureLinesGenerated(mJobResults);
        mJobResults.clear();
    }

    if (mReceivedJobResults == mAreaSize.height()) {
        qDebug() << "Generated in " << mTimer.elapsed() << " ms";
    }
}

このスロットは、ジョブがそのタスクを完了するたびに呼び出されます。最初にチェックするのは、現在の入力データで現在のJobResultがまだ有効であるかどうかです。新しい画像が要求されたら、ジョブキューをクリアしてアクティブなジョブを中止します。しかし、古いJobResultがまだこのprocess()スロットに送られている場合は無視しなければなりません。

その後、mReceivedJobResultsカウンタをインクリメントし、このJobResultsをメンバーキューであるmJobResultsに追加します。計算機は、pictureLinesGenerated()シグナルを発してそれらをディスパッチする前に、JOB_RESULT_THRESHOLD(つまり、10)結果を取得するのを待ちます。この値は注意して微調整してみてください。

また、イベントがディスパッチされると、ジョブ結果のあるQListクラスがコピーで送られてくることにも注目してください。しかし、QtはQListと暗黙の共有を行っているので、コストのかかる深いコピーではなく、浅いコピーを送るだけです。そして、電卓の現在のQListをクリアします。

最後に、処理されたJobResultが領域内の最後のものである場合、ユーザがgeneratePicture()を呼び出してからの経過時間を表示したデバッグメッセージを表示します。


Qt Tip

QThreadPoolクラスが使用するスレッド数は、setMaxThreadCount(x)で設定できます。


戻る