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

提供:MochiuWiki : SUSE, EC, PCB
2021年2月5日 (金) 03:33時点におけるWiki (トーク | 投稿記録)による版 (→‎スレッドの作成(非推奨) :)
ナビゲーションに移動 検索に移動

概要

各プログラムは起動時に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クラスを使用することもある。



スレッドの作成(非推奨)

以下に示す方法は、スレッドに低機能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とは、ミューテックス、読み込み / 書き込みロック、待機条件、セマフォ等の低機能のスレッドプリミティブを使用せずに、
マルチスレッドを作成できる高機能APIである。

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

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

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

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


 // main.cppファイル
 
 #include <QCoreApplication>
 #include <qtconcurrentrun.h>
 #include <QThread>
 
 #ifndef QT_NO_CONCURRENT
 
 void myRunFunction(QString name)
 {
    for(int i = 0; i <= 5; i++)
    {
       qDebug() << name << " " << i << "from" << QThread::currentThread();
    }
 }
 
 int main(int argc, char *argv[])
 {
    QCoreApplication a(argc, argv);
 
    QFuture<void> t1 = QtConcurrent::run(myRunFunction, QString("A"));
    QFuture<void> t2 = QtConcurrent::run(myRunFunction, QString("B"));
    QFuture<void> t3 = QtConcurrent::run(myRunFunction, QString("C"));
 
    t1.waitForFinished();
    t2.waitForFinished();
    t3.waitForFinished();
 
    return a.exec();
 }
 
 #else
 
 #include <QLabel>
 
 int main(int argc, char *argv[])
 {
    QApplication app(argc, argv);
    QString text("Qt Concurrent is not yet supported on this platform");
 
    QLabel *label = new QLabel(text);
    label->setWordWrap(true);
 
    label->show();
    qDebug() << text;
 
    app.exec();
 }
 #endif



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

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

一般的に、スレッドの優先順位を設定する部分を除いて、"スレッドの作成(非推奨)"セクションと同じである。
優先度は、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


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