Qt6の新機能 Promiseモードを使ってみる


初めに

Qt Concurrentの「Promiseモード」について紹介します。
※拙著Qt5/Qt6入門 C++編に、モードの名前だけで説明を入れられなかった為、簡単にですがこちらで説明します。

Qt6におけるQt Concurrentの2つのモード

Promiseモード(promise mode)」は、Qt6から追加されたQt Concurrentの新機能で、スレッド側の処理の進行状況と結果を非同期的にQFutureに伝達する簡単な方法を提供します。今まで(Qt5まで)のConcurrent Runとして実装されていた機能を「Basicモード(basic mode)」と位置づけ、それとは異なるモードとしてPromiseモードが追加されました。

そこで、Basicモード(basic mode)とPromiseモード(promise mode)それぞれの実装方法について、Qt Concurrent runを例に見ていきます。

Basicモード(basic mode)のQt Concurrent run

Basicモード(basic mode)を軽くおさらいします。

private:
  static void work();

Qt Concurrent呼び出し

QtConcurrent::run(work);

staticな関数を用意し、その関数をQtConcurrent::run()の引数に渡します。
すると、work()が別スレッドで実行されます。

QFuture future = QtConcurrent::run(work);

QtConcurrent::run()はQFutureを返します。QFutureはテンプレートクラスです。スレッド側の処理結果がQFutureに格納されます。QtConcurrent::run()の返り値であるQFutureを取得します。QFuture.result()で処理結果を取得できます。

QFutureWatcher<int> m_watcher; //クラス変数として定義

m_watcher.setFuture(QtConcurrent::run(work));

QFutureをQFutureWatcherに設定することにより、シグナル・スロットでスレッド側の動作状況を取得できます。
QFutureWatcherクラス(正確にはQFutureWatcherクラスの親クラスであるQFutureWatcherBaseクラス)に、スレッドの状況を取得できる関数が用意されています。

  • bool isCanceled() const
  • bool isFinished() const
  • bool isRunning() const
  • bool isStarted() const

他にもシグナル・スロットも用意されています。

Promiseモード

※Promiseモードを使用したサンプルは、GitHubに上げてあります。

PromiseモードはQPromiseクラスを使用した機能です。Promiseモードでは、QtConcurrent::run()の引数に渡す関数を

private:
  static void work();

から

private:
  static void work(QPromise<int> &promise);

のように、第1引数にQPromiseを渡すことでPromiseモードになります。

QPromiseクラスのドキュメントに記載されている例だけを見ると、あたかもQt Concurrent起動側でQPromiseを生成し、それをQt Concurrent側に必ず渡さなければならないように見えますが、そんなことはありません。
Concurrent Run With Promiseの例に記載されているように、Qt Concurrent起動側でQPromiseを生成せずに、ただQtConcurrent::run()の引数に渡す関数の第1引数に、QPromiseを追加するだけでもPromiseモードとして実行できます。
※QPromiseを生成して渡す実装方法については、QPromiseクラスのスニペットを参考にしてください(本記事では省略します)。

Promiseサンプルの説明

私が作成したサンプルでは、Qt ConcurrentのSuspend/Pauseの動作を確認することができるサンプルとなっています。


Stopボタンが押された場合は完全に終了し、最初から処理をやり直します。Suspendボタンが押された場合は処理を中断し、Resumeボタンを押下すると、中断されたところから再開します。

サンプルコードの実装内容について見ていきます。

void Widget::work(QPromise<int> &promise)
{
    for (int i = 0; i <= 100; i = i + 10) {
        QThread::sleep(1);
        promise.setProgressValue(i); //【1】
        promise.suspendIfRequested(); //【2】
        if (promise.isCanceled()) { //【3】
            qDebug() << "isCanceled() is true.";
            return;
        }
    }
}

【1】promise.setProgressValue(i)で進捗状況を呼び出し元に送信しています。これを

    connect(&m_watcher, &QFutureWatcherBase::progressValueChanged,
            ui->progressBar, &QProgressBar::setValue);

のようにシグナルprogressValueChanged()をスロットでキャッチすることで、プログレスバーの進捗率を変更することができます。

【2】promise.suspendIfRequested()で、もしQt Concurrent起動側でQFutureWatcher.suspend()を呼んでいた場合は処理を中断します。QFutureWatcher.resume()が呼ばれると処理を再開します。
suspend()はQt6.0から追加された関数です。

【3】QFutureWatcher.cancel()が呼ばれると、promise.isCanceled()がtrueになります。QFutureWatcher.cancel()が呼ばれた場合、処理は途中から再開するのではなく、最初からやり直しになります。

最後に

簡単にですが、Promiseモードについて説明しました。
Qt6には、Qt Concurrent runと同じくらい実装が簡単で、かつ複雑な設定も追加できる「Qt Concurrent task」という機能も実装されています。(こちらのBasicモードについては拙著Qt5/Qt6入門 C++編で説明しています)。
Qt Concurrentに関しては、Qt6でだいぶ良い方向に改善された印象です。
ぜひPromiseモードを使用してみてください。