Qtの基礎 - マルチスレッド

提供:MochiuWiki : SUSE, EC, PCB
ナビゲーションに移動 検索に移動

概要

各プログラムは起動時に1つのスレッドを持っている。
Qtでは、このスレッドのことをメインスレッドまたはGUIスレッドと呼ぶ。
Qt GUIでは、このスレッドで実行する必要がある。

全てのウィジェットやQPixmap等のいくつかのクラスは、セカンダリスレッドでは動作しない。
一般的に、セカンダリスレッドのことをワーカースレッドと呼ぶが、これはメインスレッドから処理をオフロードするために使用される。

データへの同時アクセス
2つのスレッドが同じオブジェクトへのポインタを持つ場合、
両方のスレッドが同時にそのオブジェクトにアクセスする可能性があるため、オブジェクトが破壊される可能性がある。
1つのオブジェクトに対する複数のスレッドによる同時操作を防止しなければならない。

マルチスレッドは、以下のような2つの使用例がある。

  • マルチコアプロセッサを利用して処理を高速化する。
  • 長い処理をオフロードまたは他のスレッドへの呼び出しをブロックすることで、GUIスレッドやその他の時間的に重要なスレッドの応答性を維持する。



マルチスレッドの代替方法

マルチスレッドを作成する前に、考えられる代替案を検討する必要がある。
下表は、Threading Basicから引用したものである。

表. マルチスレッドの代替方法

代替案 説明
QEventLoop::processEvents() 時間が掛かる処理内において、QEventLoop::processEvents()を繰り返し呼び出すことで、GUIのブロッキングを防ぐことができる。
ただし、ハードウェアによっては、processEvents()の呼び出しが頻繁に発生しすぎたり、十分な頻度で発生しないことがあるため、
このソリューションはうまく拡張できない。
QSocketNotifierクラス
QNetworkAccessManagerクラス
QIODevice::readyRead()
1つまたは複数のスレッドを持ち、それぞれが低速なネットワーク接続上でブロッキングリードを行う代替手段である。
ネットワークデータに応答する計算を素早く実行できる限り、このリアクティブ設計は、スレッドで同期的に待機するよりも優れている。
リアクティブ設計は、スレッド処理よりもエラーが発生しにくく、エネルギー効率が高い。
多くの場合、パフォーマンス面でもメリットがある。
QTimerクラス バックグラウンド処理は、将来のある時点でスロットの実行をスケジュールするために、タイマを使用して便利に行うことができる。
インターバルが0のタイマは、処理すべきイベントがなくなるとすぐにタイムアウトする。



マルチスレッドの設計方法

Qtにおいて、推奨されるマルチスレッドの設計方法を示す。
下表は、Threading Basicから引用したものである。

表. 推奨されるマルチスレッドの設計方法

スレッドの寿命 設計方法 解決策
1度のみ 別のスレッド内でメソッドを実行して、
メソッドが終了した時にスレッドを終了する。

Qtでは異なる解決策が用意されている。

  1. メソッド内において、QtConcurrent::run()を実行する。
  2. QRunnableクラスを継承したクラスを作成して、
    QThreadPool::globalInstance()->start()を実行する。
    (グローバルスレッドプール内)
  3. QThreadクラスを継承したクラスを作成して、runメソッドをオーバーライドする。
    次に、QThread::start()を実行する。
1度のみ 長時間実行している操作は、別のスレッドに入れる必要がある。
処理の途中で、GUIスレッドにステータス情報を送る必要がある。

まず、QThreadクラスを継承したクラスを作成して、runメソッドをオーバーライドする。
オーバーライドしたrunメソッド内で、シグナルを送信する。
次に、そのシグナルをGUIスレッドのスロットに接続する。

1度のみ 操作は、コンテナの全ての項目に対して行う。
処理は、利用可能な全てのコアを使用して実行する必要がある。
例: 画像のリストからサムネイルを生成する等。

QtConcurrentクラスでは、コンテナの要素ごとに操作を適用するmapメソッド、
コンテナの要素を選択するfilterメソッド、残りの要素を結合するreduceメソッド等、
指定するオプションが用意されている。

永久 オブジェクトを別のスレッドに常駐させて、
要求に応じて異なるタスクを実行する。
つまり、ワーカースレッドとの通信が必要になる。

QObjectクラスを継承したクラスを作成して、必要なスロットとシグナルを実装する。
オブジェクトを実行中のイベントループを持つスレッドに移動して、
シグナル / スロット接続を介してオブジェクトと通信する。

永久 オブジェクトを別のスレッドに常駐させて、ポートのポーリングや、
GUIスレッドと通信できるタスク等を繰り返し実行する。

上記と同様であるが、ワーカースレッドにてタイマを使用してポーリングを実装する。
ただし、ポーリングによる解決策は止めた方がよい。
QSocketNotifierクラスを使用することもある。



スレッドの作成 : QThreadクラスの使用 (非推奨)

以下に示す方法は、スレッドに低機能APIを使用しているだけでなく、スケーリングの問題が発生する可能性があるため非推奨としている。

Qtにおいて、マルスレッドを設計する場合は、高機能APIを備えたQtConcurrentを使用することを推奨する。
QtConcurrentは、スレッドの実行をキャンセル、一時停止、再開することができる。

ただし、低機能APIについても知る必要がある。

以下の例では、QThreadクラスを継承して、派生クラスを作成している。
3つのスレッドが同時に実行されて、同じコードセグメントにアクセスしている。

 // main.cpp
 
 #include <QCoreApplication>
 #include "mythread.h"
 
 int main(int argc, char *argv[])
 {
    QCoreApplication a(argc, argv);
    
    MyThread thread1("A"), thread2("B"), thread3("C");
 
    thread1.start();
    thread2.start();
    thread3.start();
 
    return a.exec();
 }


QThreadクラスのstartメソッドは、派生クラスでオーバーライドしたrunメソッドを呼び出すことにより、スレッドの実行を開始する。

 // mythread.h
 
 #ifndef MYTHREAD_H
 #define MYTHREAD_H
 
 #include <QThread>
 #include <QString>
 
 class MyThread : public QThread
 {
 public:
    // constructor
    // set name using initializer
    explicit MyThread(QString s);
 
    // overriding the QThread's run() method
    void run();
 private:
    QString name;
 };
 
 #endif // MYTHREAD_H


 // mythread.cpp
 
 #include "mythread.h"
 #include <QDebug>
 
 MyThread::MyThread(QString s) : name(s)
 {
 }
 
 // We overrides the QThread's run() method here
 // run() will be called when a thread starts
 // the code will be shared by all threads
 
 void MyThread::run()
 {
    for(int i = 0; i <= 100; i++)
    {
       qDebug() << this->name << " " << i;
    }
 }



スレッドの作成 : QtConcurrentクラスの使用 (推奨)

QtConcurrentクラスとは

QtConcurrentとは、ミューテックス、読み込み / 書き込みロック、待機条件、セマフォ等の低機能のスレッドプリミティブを使用せずに、
マルチスレッドを作成できる高機能APIである。

QtConcurrentを使用して作成したスレッドは、使用可能なプロセッサコアの数に応じて、使用されるスレッドの数を自動的に調整する。
つまり、現在作成されているアプリケーションは、将来マルチコアシステムに導入された場合でも拡張を続ける。

 # .proファイル
 
 QT       += concurrent


 # CMakeLists.txtファイル
 
 find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Core Concurrent)
 find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core Concurrent)
 
 target_link_libraries(<ターゲット名> PRIVATE
    Qt${QT_VERSION_MAJOR}::Core
    Qt${QT_VERSION_MAJOR}::Concurrent
 )


戻り値がある場合

 #include <QCoreApplication>
 #include <QtConcurrent>
 #include <QFuture>
 #include <QThread>
 #include <QDebug>
 
 int task1()
 {
    qDebug() << "Task 1 started in thread:" << QThread::currentThreadId();
 
    QThread::sleep(5);  // 5秒待機
 
    qDebug() << "Task 1 finished";
 
    return 10;
 }
 
 QString task2()
 {
    qDebug() << "Task 2 started in thread:" << QThread::currentThreadId();
 
    QThread::sleep(10);  // 10秒待機
 
    qDebug() << "Task 2 finished";
 
    return "Task 2 finished";
 }
 
 int main(int argc, char *argv[])
 {
    QCoreApplication a(argc, argv);
 
    qDebug() << "Main thread: " << QThread::currentThreadId();
 
    // 2つのタスクを非同期で実行
    QFuture<int>     future1 = QtConcurrent::run(task1);
    QFuture<QString> future2 = QtConcurrent::run(task2);
 
    // 両方のタスクが完了するまで待機して、結果を取得
    // QFuture::resultメソッドはタスクが完了するまでブロックするため、waitForFinishedメソッドを明示的に呼び出す必要がない
    int     result1 = future1.result();
    QString result2 = future2.result();
 
    qDebug() << "Task 1 result: " << result1;
    qDebug() << "Task 2 result: " << result2;
 
    qDebug() << "All tasks completed";

    return a.exec();
 }


戻り値が無い場合

以下の例では、上記のセクションと同じ出力を取得している。
設計者がスレッドを作成する必要はなく、適切なパラメータを使用して関数を定義するだけである。

QtConcurrentのAPIは、以下の通りである。

  • QtConcurrent::run()
    別のスレッドで関数を実行する。
  • QFutureクラス
    非同期処理の結果を表す。
    QFutureクラスは、cancel()pause()resume()等の実行中の計算と対話する方法も提供している。
    ただし、以下の例では、QtConcurrent::run()QFutureクラスを返すため、これを行うことはできない。
    これについては、QtConcurrent::mappedReduced()の後半のセクションを参照すること。


 #include <QCoreApplication>
 #include <QtConcurrent>
 #include <QFuture>
 #include <QThread>
 
 void func(QString name)
 {
    for(auto i = 0; i <= 5; i++) {
       qDebug() << name << " " << i << "from" << QThread::currentThread();
    }
 }
 
 int main(int argc, char *argv[])
 {
    QCoreApplication a(argc, argv);
 
    // QtConcurrent::runメソッドを使用して、func関数を別々のスレッドで非同期に実行
    // また、QFutureクラスのオブジェクトを使用して、各タスクの実行状況を追跡
    QFuture<void> t1 = QtConcurrent::run(func, QString("A"));
    QFuture<void> t2 = QtConcurrent::run(func, QString("B"));
    QFuture<void> t3 = QtConcurrent::run(func, QString("C"));

    // waitForFinishedメソッドを使用して、両方のタスクが完了するまで待機 
    t1.waitForFinished();
    t2.waitForFinished();
    t3.waitForFinished();
 
    return a.exec();
 }


クラスのメソッドとして定義する場合

以下の例では、task1およびtask2をクラスのメンバ関数として定義して、非同期で実行している。

  • このアプローチの利点
    タスクに関連する機能を1つのクラスにカプセル化することができる。
    QObjectクラスを継承することにより、Qtのシグナル・スロットを使用できるようになる。
    クラスのメンバ変数等をタスク間で状態を共有することが可能になる。


  • 非同期メソッドをstaticにする場合のメリット
    • メリット
      オブジェクトのインスタンスを作成せずに直接メソッドを呼び出すことができる。
      メモリ使用量が若干少なくなる可能性がある。 (オブジェクトを作成しないため)

  • 非同期メソッドをstaticにする場合のデメリット
    オブジェクトの状態 (メンバ変数) にアクセスできない。
    QObjectクラスの一部の機能 (例: シグナルの発行) を直接使用することができない。


非staticとして定義することにより、将来的な拡張性が高まることが多い。
ただし、非同期メソッドが常に同じ方法で動作し、かつ、オブジェクトの状態に依存しないことが確実な場合は、staticとして定義しても問題ない。

 // Task.hファイル
 
 #include <QCoreApplication>
 #include <QObject>
 #include <QtConcurrent>
 #include <QFuture>
 #include <QThread>
 #include <QDebug>
 
 class Task : public QObject
 {
    Q_OBJECT
 
 public:
    Task(QObject *parent = nullptr) : QObject(parent) {}
 
    int task1()
    {
        qDebug() << "Task 1 started in thread:" << QThread::currentThreadId();
 
        QThread::sleep(5);  // 5秒待機
 
        qDebug() << "Task 1 finished";
 
        return 10;
    }
 
    QString task2()
    {
        qDebug() << "Task 2 started in thread:" << QThread::currentThreadId();
 
        QThread::sleep(10);  // 10秒待機
 
        qDebug() << "Task 2 finished";
 
        return "Task 2 finished";
    }
 };


 // main.cppファイル
 
 #include "Task.h"
 
 int main(int argc, char *argv[])
 {
    QCoreApplication a(argc, argv);
 
    qDebug() << "Main thread: " << QThread::currentThreadId();
 
    Task task;
 
    // 2つのタスクを非同期で実行
    QFuture<int>     future1 = QtConcurrent::run(&task, &Task::task1);
    QFuture<QString> future2 = QtConcurrent::run(&task, &Task::task2);
 
    // 両方のタスクが完了するまで待機して、結果を取得
    // QFuture::resultメソッドはタスクが完了するまでブロックするため、waitForFinishedメソッドを明示的に呼び出す必要がない
    int result1     = future1.result();
    QString result2 = future2.result();
 
    qDebug() << "Task 1 result: " << result1;
    qDebug() << "Task 2 result: " << result2;
 
    qDebug() << "All tasks completed";
 
    return a.exec();
 }


スレッドの監視 (QFutureWatcherクラス)

  • isFinishedメソッド
    操作が完了したかどうかを確認する。
  • isRunningメソッド
    操作が現在実行中かどうかを確認する。
  • isCanceledメソッド
    操作がキャンセルされたかどうかを確認する。
  • resultメソッド
    操作の結果を取得する。
    また、操作が完了するまでブロックする。


以下の例では、QtConcurrent::runを使用して非同期操作を開始して、その結果をQFutureオブジェクトで受け取る。
QFutureWatcherクラスを使用して操作の完了を監視てし、完了時にoperationFinishedスロットが呼び出している。

また、QFutureクラスは、複数の結果を持つ操作にも対応しており、resultsメソッドを使用して全ての結果を取得することができる。

 #include <QtConcurrent>
 #include <QFuture>
 #include <QFutureWatcher>
 
 // 長時間実行される関数
 int longOperation(int input)
 {
    // 重い処理をシミュレート
    QThread::sleep(5);
    return input * 2;
 }
 
 // メインウィンドウのクラス等
 void MainWindow::startOperation()
 {
    QFuture<int> future = QtConcurrent::run(longOperation, 42);
 
    // QFutureWatcherを使用して、操作の完了を監視
    QFutureWatcher<int>* watcher = new QFutureWatcher<int>(this);
    connect(watcher, &QFutureWatcher<int>::finished, this, &MainWindow::operationFinished);
    watcher->setFuture(future);
 }
 
 void MainWindow::operationFinished()
 {
    QFutureWatcher<int>* watcher = qobject_cast<QFutureWatcher<int>*>(sender());
 
    if (watcher) {
       QFuture<int> future = watcher->future();
       if (future.isFinished() && !future.isCanceled()) {
          int result = future.result();
          qDebug() << "Operation result:" << result;
       }
       watcher->deleteLater();
    }
 }


継続処理

QFutureクラスにはthenメソッド (Qt 5.10以降で導入) が存在する。

thenメソッドは、非同期処理が完了した後に実行される継続処理を追加するために使用する。
これにより、非同期操作のチェーンを作成して、可読性を高めて、管理しやすくすることができる。

 // thenメソッドのシグネチャ
 
 QFuture<NewType> QFuture::then(Function function)


thenメソッドのメリットを以下に示す。

  • コードの可読性向上
    非同期操作の流れが明確になる。
  • エラー処理の簡素化
    各ステップでエラーをキャッチして処理することができる。
  • 複数の操作の連鎖
    複数のthenメソッドを連続して使用することにより、複雑な非同期処理フローを構築できる。


また、thenメソッドを使用することにより、非同期処理がより直感的になり、コールバックの入れ子を避けることができる。

※注意
thenメソッドは新しいQFutureオブジェクトを返すため、元のQFutureとは別のオブジェクトになる。
thenメソッドに渡される関数は、元の操作が完了した後で実行される。

以下の例では、longOperation関数を非同期で実行して、その結果を含むQFuture<int>を取得している。
thenメソッドを使用して、longOperationの結果を処理する継続処理を追加している。

継続処理はprocessResult関数を呼び出し、結果を文字列に変換する。
最後に、QFuture<QString>が返されて、その結果を待機して出力する。

 #include <QtConcurrent>
 #include <QFuture>
 #include <QDebug>
 
 int longOperation(int input)
 {
    // 重い処理をシミュレート
    QThread::sleep(5);
    return input * 2;
 }
 
 QString processResult(int result)
 {
    return QString("The result is: %1").arg(result);
 }
 
 void exampleUsage()
 {
    QFuture<int> future = QtConcurrent::run(longOperation, 21);
 
    QFuture<QString> finalFuture = future.then([](int result) {
       return processResult(result);
    });
 
    // 結果が返るまで待機
    finalFuture.waitForFinished();
    qDebug() << finalFuture.result();
 }


一時停止 / キャンセル / 再開

  • pauseメソッド
    実行中の操作を一時停止する。
  • cancelメソッド
    実行中の操作をキャンセルする。
  • resumeメソッド
    一時停止された操作を再開する。


以下の例では、QFutureクラスを使用して、長時間実行される操作をシミュレートして、その操作を制御するためのUIを提供している。

 // MainWindow.h
 
 #include <QMainWindow>
 #include <QPushButton>
 #include <QProgressBar>
 #include <QVBoxLayout>
 #include <QFuture>
 #include <QFutureWatcher>
 #include <QtConcurrent>
 #include <QDebug>
 
 class MainWindow : public QMainWindow
 {
    Q_OBJECT
 
 private:
    QPushButton          *startButton;
    QPushButton          *cancelButton;
    QPushButton          *pauseButton;
    QPushButton          *resumeButton;
    QProgressBar         *progressBar;
    QFuture<void>        future;
    QFutureWatcher<void> *watcher;
 
    void setupUi()
    {
       QWidget *centralWidget = new QWidget(this);
       QVBoxLayout *layout    = new QVBoxLayout(centralWidget);
 
       startButton  = new QPushButton("Start", this);
       cancelButton = new QPushButton("Cancel", this);
       pauseButton  = new QPushButton("Pause", this);
       resumeButton = new QPushButton("Resume", this);
       progressBar  = new QProgressBar(this);
 
       layout->addWidget(startButton);
       layout->addWidget(cancelButton);
       layout->addWidget(pauseButton);
       layout->addWidget(resumeButton);
       layout->addWidget(progressBar);
 
       setCentralWidget(centralWidget);
 
       connect(startButton, &QPushButton::clicked, this, &MainWindow::startOperation);
       connect(cancelButton, &QPushButton::clicked, this, &MainWindow::cancelOperation);
       connect(pauseButton, &QPushButton::clicked, this, &MainWindow::pauseOperation);
       connect(resumeButton, &QPushButton::clicked, this, &MainWindow::resumeOperation);
 
       cancelButton->setEnabled(false);
       pauseButton->setEnabled(false);
       resumeButton->setEnabled(false);
    }
 
 public:
    MainWindow(QWidget *parent = nullptr) : QMainWindow(parent), watcher(nullptr)
    {
       setupUi();
    }
 
 private slots:
    void startOperation()
    {
       if (watcher) {
          delete watcher;
       }
       watcher = new QFutureWatcher<void>(this);
       connect(watcher, &QFutureWatcher<void>::progressValueChanged, this, &MainWindow::updateProgress);
       connect(watcher, &QFutureWatcher<void>::finished, this, &MainWindow::operationFinished);
 
       future = QtConcurrent::run([this]() {
          for (int i = 0; i <= 100; i++) {
             QThread::msleep(100);  // 時間の掛かる処理
             if (QThread::currentThread()->isInterruptionRequested()) {
                return;
             }
             QtConcurrent::reportProgress(i);
          }
       });
 
       watcher->setFuture(future);
 
       startButton->setEnabled(false);
       cancelButton->setEnabled(true);
       pauseButton->setEnabled(true);
       resumeButton->setEnabled(false);
    }
 
    void cancelOperation()
    {
       if (future.isRunning()) {
          future.cancel();
       }
    }
 
    void pauseOperation()
    {
       if (future.isRunning()) {
          future.pause();
          pauseButton->setEnabled(false);
          resumeButton->setEnabled(true);
       }
    }
 
    void resumeOperation()
    {
       if (future.isPaused()) {
          future.resume();
          pauseButton->setEnabled(true);
          resumeButton->setEnabled(false);
       }
    }
 
    void updateProgress(int value)
    {
       progressBar->setValue(value);
    }
 
    void operationFinished()
    {
       if (future.isCanceled()) {
          qDebug() << "Operation was canceled";
       }
       else {
          qDebug() << "Operation completed successfully";
       }
 
       startButton->setEnabled(true);
       cancelButton->setEnabled(false);
       pauseButton->setEnabled(false);
       resumeButton->setEnabled(false);
    }
 };


 // main.cpp
 
 #include <QApplication>
 #include "MainWindow.h"
 
 int main(int argc, char *argv[])
 {
    QApplication a(argc, argv);
    MainWindow w;
    w.show();
    return a.exec();
 }


進捗

  • progressValueメソッド
    進捗状況の現在の値を取得する。
  • progressMinimumメソッド
    進捗状況の最小値を取得する。
  • progressMaximum
    進捗状況の最大値を取得する。


以下の例では、QFutureクラスの進捗状況関連のメソッドの使用方法を示している。
progressValueメソッド、progressMinimumメソッド、progressMaximumメソッドを使用することにより、
非同期操作の進捗状況をより詳細に追跡して、ユーザに表示、あるいは、ログに記録することができる。

  1. [Start]ボタンを押下すると、非同期操作が開始される。
  2. プログレスバーが更新されて、現在の進捗値、最小値、最大値がラベルに表示される。
  3. コンソールに詳細な進捗情報が出力される。
    これには、progressValueメソッド、progressMinimumメソッド、progressMaximumメソッドから取得した値が含まれる。
  4. 操作が完了する時、結果がログに記録されて、[Start]ボタンが再度有効になる。


 // ProgressWindow.hファイル
 
 #include <QMainWindow>
 #include <QPushButton>
 #include <QProgressBar>
 #include <QLabel>
 #include <QVBoxLayout>
 #include <QFuture>
 #include <QFutureWatcher>
 #include <QtConcurrent>
 #include <QDebug>
 
 class ProgressWindow : public QMainWindow
 {
    Q_OBJECT
 
 private:
    QPushButton  *startButton;
    QProgressBar *progressBar;
    QLabel       *currentValueLabel;
    QLabel       *minValueLabel;
    QLabel       *maxValueLabel;
    QFuture<void>        future;
    QFutureWatcher<void> *watcher;
 
    void setupUi()
    {
       QWidget *centralWidget = new QWidget(this);
       QVBoxLayout *layout    = new QVBoxLayout(centralWidget);
 
       startButton       = new QPushButton("Start", this);
       progressBar       = new QProgressBar(this);
       currentValueLabel = new QLabel("Current: 0", this);
       minValueLabel     = new QLabel("Min: 0", this);
       maxValueLabel     = new QLabel("Max: 0", this);
 
       layout->addWidget(startButton);
       layout->addWidget(progressBar);
       layout->addWidget(currentValueLabel);
       layout->addWidget(minValueLabel);
       layout->addWidget(maxValueLabel);
 
       setCentralWidget(centralWidget);
 
       connect(startButton, &QPushButton::clicked, this, &ProgressWindow::startOperation);
    }
 
 public:
    ProgressWindow(QWidget *parent = nullptr) : QMainWindow(parent), watcher(nullptr)
    {
       setupUi();
    }
 
 private slots:
    void startOperation()
    {
       if (watcher) {
          delete watcher;
       }
       watcher = new QFutureWatcher<void>(this);
       connect(watcher, &QFutureWatcher<void>::progressRangeChanged, this, &ProgressWindow::updateProgressRange);
       connect(watcher, &QFutureWatcher<void>::progressValueChanged, this, &ProgressWindow::updateProgressValue);
       connect(watcher, &QFutureWatcher<void>::finished, this, &ProgressWindow::operationFinished);
 
       future = QtConcurrent::run([this]() {
          const int totalSteps = 100;
          for (int i = 0; i <= totalSteps; ++i) {
             QThread::msleep(100);  // シミュレートされた作業
             if (QThread::currentThread()->isInterruptionRequested()) {
                return;
             }
             QtConcurrent::reportProgress(i, totalSteps);
          }
       });
 
       watcher->setFuture(future);
       startButton->setEnabled(false);
    }
 
    void updateProgressRange(int minimum, int maximum)
    {
       progressBar->setRange(minimum, maximum);
       minValueLabel->setText(QString("Min: %1").arg(minimum));
       maxValueLabel->setText(QString("Max: %1").arg(maximum));
    }
 
    void updateProgressValue(int value)
    {
       progressBar->setValue(value);
       currentValueLabel->setText(QString("Current: %1").arg(value));
 
       // QFutureのメソッドを使用して進捗状況を取得
       int progressValue   = future.progressValue();
       int progressMinimum = future.progressMinimum();
       int progressMaximum = future.progressMaximum();
 
       qDebug() << "Progress:" << progressValue << "/" << progressMaximum
                << "(" << progressMinimum << "-" << progressMaximum << ")";
    }
 
    void operationFinished()
    {
       if (future.isCanceled()) {
          qDebug() << "Operation was canceled";
       }
       else {
          qDebug() << "Operation completed successfully";
       }
       startButton->setEnabled(true);
    }
 };


 // main.cppファイル
 
 #include <QApplication>
 #include "ProgressWindow.h"
 
 int main(int argc, char *argv[])
 {
    QApplication a(argc, argv);
    ProgressWindow w;
    w.show();
    return a.exec();
 }


音声ファイルの再生が終了するまで待機

以下の例では、音声ファイルの再生が終了するまで待機している。

 // MainWindow.cpp
 
 #include <QtConcurrent>
 #include <QtMultimedia>
 
 void MainWindow::PlaySound()
 {
    QFuture<void> Task = QtConcurrent::run([]()
    {
       QSoundEffect effect;
       QEventLoop loop;
       QString strFilePath = QCoreApplication::applicationDirPath() + QDir::separator() + tr("Sample.wav");
       effect.setSource(QUrl::fromLocalFile(strFilePath));
       effect.setVolume(50);
       effect.play();
       QObject::connect(&effect, &QSoundEffect::playingChanged, [&loop](){loop.exit();});
       loop.exec();
    });
    Task.waitForFinished();
 }



スレッドのプライオリティ

優先度に応じて、スレッドがどのように動作するかを記載する。

一般的に、スレッドの優先順位を設定する部分を除いて、"スレッドの作成(非推奨)"セクションと同じである。
優先度は、QThreadクラスのstartメソッドに渡されるパラメータにより設定される。
その定義は、以下のようになる。

 void QThread::start(Priority priority = InheritPriority)


startメソッドは、runメソッドを呼び出してスレッドの実行を開始する。(既にスレッドが実行されている場合、startメソッドはNOP)
OSは、優先度パラメータにしたがってスレッドをスケジュールする。

優先度パラメータの効果は、OSのスケジューリングポリシーによって異なる。
特に、スレッドの優先順位をサポートしていないシステムでは、優先順位は無視される。

表. enum QThread::Priority

定数 説明
QThread::IdlePriority 0 他のスレッドが実行されていない場合にのみ、スケジュールされる。
QThread::LowestPriority 1 LowPriorityよりも少ない頻度でスケジュールされる。
QThread::NormalPriority 3 OSの標準の優先度。
QThread::HighestPriority 5 HighPriorityよりも頻繁にスケジュールされる。
QThread::QThread::InheritPriority 7 作成したスレッドと同じ優先度を使用する。(初期状態)


 // main.cppファイル
 
 #include <QCoreApplication>
 #include "mythread.h"
 
 int main(int argc, char *argv[])
 {
    QCoreApplication a(argc, argv);
 
    MyThread thread1("A"), thread2("B"), thread3("C");
 
    thread1.start(QThread::LowestPriority);
    thread2.start();
    thread3.start(QThread::HighestPriority);
 
    return a.exec();
 }


上記のサンプルコードの実行結果から、SUSEのスケジューラは、スレッドの優先順位を尊重しているようである。
プライオリティの低い"A"スレッドが最後に終了している。

...略
"A"   96
"A"   97
"A"   98
"A"   99
"A"   100


優先順位の逆転の問題がある場合、 優先順位の逆転を参照すること。


QMutexクラス

QMutextクラスとは

QMutexクラスは、スレッド間のアクセスシリアル化を提供する。
QMutexの目的は、オブジェクト、データ構造、ソースコードのセクションを保護して、1度に1つのスレッドのみがアクセスできるようにする。(同期)

一般的に、QMutexLockerクラスを使用することが最適である。
これにより、ロックとロック解除が一貫して実行されるようになる。

 QMutex::QMutex(RecursionMode mode = NonRecursive)


  1. QMutexクラスのインスタンスを生成して、新しいミューテックスを構築する。
    ミューテックスは、ロック解除された状態で生成される。
  2. ミューテックスのモードがQMutex::Recursiveの場合、スレッドは同じミューテックスを複数回ロックでき、
    対応する数のunlockメソッドの呼び出しが行われるまで、ミューテックスのロックは解除されない。
  3. それ以外の場合、スレッドはミューテックスを1回だけロックできる。
    モードを指定しない場合は、QMutex::NonRecursiveが指定される。


表. enum QMutex::RecursionMode

定数 説明
QMutex::Recursive 1 スレッドは、同じミューテックスを複数回ロックでき、
対応する数のunlockメソッドの呼び出しが行われるまで、ミューテックスはロック解除されない。
QMutex::NonRecursive 0 スレッドは、ミューテックスを1回だけロックできる。


変数の排他制御

以下の例では、スレッドの動作を制御するためのメンバ変数bStopを定義している。
変数bStopがtrueの場合、スレッドはループから抜ける。
したがって、1つのスレッドからのみアクセスする必要があり、ミューテックスのロックを使用している。

 // main.cppファイル
 
 #include <QCoreApplication>
 #include <QDebug>
 #include "mythread.h"
 
 int main(int argc, char *argv[])
 {
    QCoreApplication a(argc, argv);
 
    // creating three thread instances
    MyThread thread1("A"), thread2("B"), thread3("C");
 
    qDebug() << "hello from GUI thread " << a.thread()->currentThreadId();
 
    // thread start -> call run()
    thread1.start();
    thread2.start();
    thread3.start();
 
    return a.exec();
 }


 // mythread.hファイル
 
 #ifndef MYTHREAD_H
 #define MYTHREAD_H
 
 #include <QThread>
 #include <QString>
 
 class MyThread : public QThread
 {
 public:
    // constructor
    // set name and Stop is set as false by default
    MyThread(QString s, bool b = false);
 
    // overriding the QThread's run() method
    void run();
 
    // variable that mutex protects
    bool bStop;
 private:
    QString name;
 };
 
 #endif // MYTHREAD_H


 // mythread.cppファイル
 
 #include "mythread.h"
 #include <QDebug>
 #include <QMutex>
 
 MyThread::MyThread(QString s, bool b) : name(s), bStop(b)
 {
 }
 
 // run() will be called when a thread starts
 void MyThread::run()
 {
    qDebug() << this->name << " " << this->Stop;
    for(int i = 0; i <= 5; i++)
    {
        QMutex mutex;
        // prevent other threads from changing the "Stop" value
        mutex.lock();
        if(this->bStop) break;
        mutex.unlock();
        qDebug() << this->name << " " << i;
    }
 }


# 出力
hello from GUI thread  0x1364
"A"   false
"C"   false
"C"   0
"A"   0
"A"   1
"A"   2
"A"   3
"A"   4
"C"   1
"A"   5
"B"   false
"C"   2
"C"   3
"C"   4
"C"   5
"B"   0
"B"   1
"B"   2
"B"   3
"B"   4
"B"   5


ファイルの排他制御 (QMutexの使用)

以下の例では、QMutexクラスを使用してファイルアクセスを同期化して、各関数でテキストファイルに排他的にアクセスして読み書きしている。

QMutexクラスのインスタンスをグローバルに定義して、ファイルアクセスの排他制御に使用する。

QMutexLockerクラスを使用することにより、ロックの取得と解放が自動的に行われて、例外が発生した場合でも確実にロックが解放される。
各関数は排他的にファイルにアクセスするため、データの競合や不整合を防ぐことができる。

※注意
タスクの実行順序は保証されないため、ファイルの最終的な内容は実行ごとに異なる可能性がある。
ファイル操作のエラー処理をより堅牢にする必要がある。

 #include <QCoreApplication>
 #include <QtConcurrent>
 #include <QFuture>
 #include <QThread>
 #include <QMutex>
 #include <QFile>
 #include <QTextStream>
 #include <QDebug>
 
 QMutex fileMutex;
 
 int readFromFile(QString &content)
 {
    QFile file("sample.txt");
    if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
        qDebug() << "Error: Unable to open file for reading";
        return -1;
    }
 
    QTextStream in(&file);
    QString content = in.readAll();
    file.close();
    return 0;
 }
 
 int writeToFile(const QString &content)
 {
    QFile file("sample.txt");
    if (!file.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Append)) {
        qDebug() << "Error: Unable to open file for writing";
        return -1;
    }
    QTextStream out(&file);
    out << content;
    file.close();
 
    return 0;
 }
 
 int task1()
 {
    QMutexLocker locker(&fileMutex);  // ロックを取得
    qDebug() << "Task 1 started in thread: " << QThread::currentThreadId();
 
    // ファイルを読み込み表示する
    QString content = "";
    readFromFile(content);
    qDebug() << "Task 1 read: " << content;
 
    // ファイルへ書き込む
    writeToFile("Task 1 was here\n");
 
    QThread::sleep(2);  // 2秒待機
    qDebug() << "Task 1 finished";
 
    return 0;  // ロッカーのデストラクタが呼ばれて、ロックが解放される
 }
 
 int task2()
 {
    QMutexLocker locker(&fileMutex);  // ロックを取得
    qDebug() << "Task 2 started in thread: " << QThread::currentThreadId();
 
    // ファイルを読み込み表示する
    QString content = "";
    readFromFile(content);
    qDebug() << "Task 2 read: " << content;
 
    writeToFile("Task 2 was here\n");
 
    QThread::sleep(5);  // 5秒待機
    qDebug() << "Task 2 finished";
 
    return 0;  // ロッカーのデストラクタが呼ばれて、ロックが解放される
 }
 
 int main(int argc, char *argv[])
 {
    QCoreApplication a(argc, argv);
 
    qDebug() << "Main thread: " << QThread::currentThreadId();
 
    // ファイル内容を設定
    writeToFile("Initial content\n");
 
    // 2つのタスクを非同期で実行
    QFuture<int> future1 = QtConcurrent::run(task1);
    QFuture<int> future2 = QtConcurrent::run(task2);
 
    // 両方のタスクが完了するまで待機し、結果を取得
    int result1 = future1.result();
    int result2 = future2.result();
 
    qDebug() << "Task 1 result:" << result1;
    qDebug() << "Task 2 result:" << result2;
 
    qDebug() << "All tasks completed";
 
    // 最終的なファイル内容を表示
    qDebug() << "Final file content:";
    qDebug() << readFromFile();
 
    return a.exec();
 }



ファイルの排他制御 (QFile::tryLockメソッドの使用)

QFileクラスのtryLockメソッドとは、指定した時間を最大待機時間として、ロックを取得しようとするものである。

bool tryLock(int timeout)


具体的には以下のような動作をする。

  1. tryLockメソッドを実行して、ファイルのロックの取得を試みる。

  2. この間、システムは繰り返しロックの取得を試みる。
    つまり、最大待機時間の間に何度もロックを確認する。

  3. 以下のいずれかにより、tryLockメソッドが終了する。
    • ロックの取得に成功した場合 (trueを返す)
    • 最大待機時間が経過してもロックが取得できなかった場合 (falseを返す)


つまり、tryLockメソッドは「最大待機時間の間に何度かロックを確認しながら待機する」という動作をする。
これにより、ファイルが利用可能になり次第すぐにロックを取得できるため、効率的にリソースを使用できる。

最大待機時間以内にロックが解放された場合は、その時点でロックを取得してメソッドは終了する。

以下の例では、複数のスレッドでファイルの読み書きを行っている。
各関数内でtryLockメソッドを使用して、3秒のタイムアウトでファイルのロックを試みている。

 #include <QCoreApplication>
 #include <QFile>
 #include <QTextStream>
 #include <QtConcurrent>
 #include <QFuture>
 #include <QFutureSynchronizer>
 #include <QDebug>
 
 QString readFile(const QString &filePath)
 {
    QFile file(filePath);
    if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
       qDebug() << "ファイルオープンエラー: " << file.errorString();
       return QString();
    }
 
    // 最大3秒まで待機
    if (!file.tryLock(3000)) {
       qDebug() << "ファイルのロックに失敗 (読み取り)";
       file.close();
       return QString();
    }
 
    QTextStream in(&file);
    QString content = in.readAll();
    qDebug() << "データ: " << content;
 
    file.unlock();  // ファイルのロックを解除
    file.close();   // ファイルを閉じる
 
    return content;
 }
 
 bool writeFile(const QString &filePath, const QString &content)
 {
    QFile file(filePath);
    if (!file.open(QIODevice::ReadWrite | QIODevice::Append | QIODevice::Text)) {
       qDebug() << "ファイルオープンエラー: " << file.errorString();
       return false;
    }
 
    // 最大3秒まで待機
    if (!file.tryLock(3000)) {
       qDebug() << "ファイルのロックに失敗 (書き込み)";
       file.close();
       return false;
    }
 
    QTextStream out(&file);
    out << content << Qt::endl;
    qDebug() << "書き込みデータ: " << content;
 
    file.unlock();  // ファイルのロックを解除
    file.close();   // ファイルを閉じる
 
    return true;
 }
 
 int main(int argc, char *argv[])
 {
    QCoreApplication a(argc, argv);
 
    QString filePath = "sample.txt";
 
    // ファイルが存在しない場合は作成
    QFile file(filePath);
    if (!file.exists()) {
       if (file.open(QIODevice::WriteOnly | QIODevice::Text)) {
          QTextStream out(&file);
          out << "Initial content" << Qt::endl;
          file.close();
       }
    }
  
    QFutureSynchronizer<void> synchronizer;
 
    // ファイルの読み込み (複数のスレッドの使用)
    for (int i = 0; i < 3; i++) {
       QFuture<QString> readFuture = QtConcurrent::run(readFile, filePath);
       synchronizer.addFuture(readFuture);
    }
 
    // ファイルの書き込み (複数のスレッドの使用)
    for (int i = 0; i < 3; i++) {
       QString content = QString("Concurrent write %1").arg(i);
       QFuture<bool> writeFuture = QtConcurrent::run(writeFile, filePath, content);
       synchronizer.addFuture(writeFuture);
    }
 
    // 全てのスレッドが完了するまで待機
    synchronizer.waitForFinished();
 
    return 0;
 }


ファイルの操作を終了する時、一般的に、QFile::unlockメソッドを先に実行して、次にQFile::closeメソッドを実行するべきである。

  • リソースの解放順序
    ロックは、ファイルハンドルに関連付けられたリソースである。
    ファイルを閉じる前にロックを解除することにより、リソースを適切に解放することができる。
  • 他のプロセスやスレッドへの配慮
    ファイルを閉じる前にロックを解除することで、他のプロセスやスレッドがファイルにアクセスできるようになる。
    これにより、リソースの効率的な利用が可能になる。
  • エラー処理の簡素化
    ロックの解除に失敗した場合、ファイルがまだ開いている状態でエラー処理を行うことができる。



スレッドの作成 : GUI

QThreadクラスの使用

ここでは、ダイアログとボタンコントロールを使用したマルチスレッドの作成方法を記載する。

以下の例では、ダイアログの[開始]ボタンを押下することでスロットがトリガーされて、スレッドのstartメソッドを呼び出す。
startメソッドは、valueChangedシグナルが発行されるスレッドのrunメソッドを呼び出す。
valueChangedシグナルはonValueChangedスロットに接続されて、ダイアログのカウントラベルを更新する。

 // main.cppファイル
 
 #include <QApplication>
 #include "dialog.h"
 
 int main(int argc, char *argv[])
 {
    QApplication a(argc, argv);
    Dialog w;
    w.show();
 
    return a.exec();
 }


 // dialog.hファイル
 
 #ifndef DIALOG_H
 #define DIALOG_H
 
 #include <QDialog>
 #include <QString>
 #include "mythread.h"
 
 namespace Ui {class Dialog;}
 
 class Dialog : public QDialog
 {
    Q_OBJECT
 
 public:
    explicit Dialog(QWidget *parent = 0);
    ~Dialog();
    MyThread *mThread;
 
 private:
    Ui::Dialog *ui;
 
 public slots:
    void onValueChanged(int);
 
 private slots:
    // for Start button
    void on_pushButton_clicked();
 
    // for Stop button
    void on_pushButton_2_clicked();
 };

 #endif // DIALOG_H


 // dialog.cppファイル
 
 #include "dialog.h"
 #include "ui_dialog.h"
 #include "mythread.h"
 
 Dialog::Dialog(QWidget *parent) : QDialog(parent), ui(new Ui::Dialog)
 {
    ui->setupUi(this);
 
    // create an instance of MyThread
    mThread = new MyThread(this);
 
    // connect signal/slot
    connect(mThread, SIGNAL(valueChanged(int)), this, SLOT(onValueChanged(int)));
 }
 
 Dialog::~Dialog()
 {
    delete ui;
 }
 
 // Absorb the signal emitted from a run() method and reflect the count change to the count label in our dialog
 void Dialog::onValueChanged(int count)
 {
    ui->label->setText(QString::number(count));
 }
 
 // Start button
 void Dialog::on_pushButton_clicked()
 {
    mThread->start();
 }
 
 // Stop button
 void Dialog::on_pushButton_2_clicked()
 {
    mThread->Stop = true;
 }


 // mythread.hファイル
 
 #ifndef MYTHREAD_H
 #define MYTHREAD_H
 
 #include <QThread>
 
 class MyThread : public QThread
 {
    Q_OBJECT
 public:
    explicit MyThread(QObject *parent = 0, bool b = false);
    void run();
 
    // if Stop = true, the thread will break
    // out of the loop, and will be disposed
    bool Stop;
 
 signals:
    // To communicate with Gui Thread
    // we need to emit a signal
    void valueChanged(int);
 };
 
 #endif // MYTHREAD_H


 // mythread.cppファイル
 
 #include "mythread.h"
 #include <QDebug>
 
 MyThread::MyThread(QObject *parent, bool b) : QThread(parent), Stop(b)
 {
 }
 
 // run() will be called when a thread starts
 void MyThread::run()
 {
    for(int i = 0; i <= 100; i++)
    {
        QMutex mutex;
        // prevent other threads from changing the "Stop" value
        mutex.lock();
        if(this->Stop) break;
        mutex.unlock();
 
        // emit the signal for the count label
        emit valueChanged(i);
 
        // slowdown the count change, msec
        this->msleep(500);
    }
 }


QtConcurrentクラスの使用

以下の例では、QtConcurrentクラスとQEventLoopクラスを使用したマルチスレッドである。

まず、.proファイルのQTにconcurrentを追記する。

# .proファイル

QT += 〜 concurrent


 #include <QtConcurrent>
 
 void MainWindow::onBtnClicked()
 {
    //-> マルチスレッド開始
    QEventLoop eventLoop;
    QtConcurrent::run([this, &eventLoop]()
    {
       heavyFunction();  // 時間の掛かる処理
       eventLoop.quit();
    });
    eventLoop.exec();
    //<- マルチスレッド終了
 
    return;
 }