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

# 初めに

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

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

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

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

# Basicモード（basic mode）のQt Concurrent run

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

```cpp
private:
  static void work();
```

Qt Concurrent呼び出し

```cpp
QtConcurrent::run(work);
```

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

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

```cpp
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](https://github.com/argama147/qt-example/tree/master/PromiseSample)に上げてあります。

Promiseモードは[QPromiseクラス](https://doc.qt.io/qt-6/qpromise.html)を使用した機能です。Promiseモードでは、QtConcurrent::run()の引数に渡す関数を

```cpp
private:
  static void work();
```

から

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

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

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

## Promiseサンプルの説明

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

![promise.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/69678/f0f4ecfc-ea26-a313-f1ba-4674e8feedb2.gif)
Stopボタンが押された場合は完全に終了し、最初から処理をやり直します。Suspendボタンが押された場合は処理を中断し、Resumeボタンを押下すると、中断されたところから再開します。

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

```cpp
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)で進捗状況を呼び出し元に送信しています。これを

```cpp
    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++編](https://github.com/argama147/qt6cppbook)で説明しています）。
Qt Concurrentに関しては、Qt6でだいぶ良い方向に改善された印象です。
ぜひPromiseモードを使用してみてください。