Qtの基礎 - マルチスレッド
概要
各プログラムは起動時に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度のみ | 長時間実行している操作は、別のスレッドに入れる必要がある。 処理の途中で、GUIスレッドにステータス情報を送る必要がある。 |
まず、QThreadクラスを継承したクラスを作成して、runメソッドをオーバーライドする。 |
1度のみ | 操作は、コンテナの全ての項目に対して行う。 処理は、利用可能な全てのコアを使用して実行する必要がある。 例: 画像のリストからサムネイルを生成する等。 |
QtConcurrentクラスでは、コンテナの要素ごとに操作を適用するmapメソッド、 |
永久 | オブジェクトを別のスレッドに常駐させて、 要求に応じて異なるタスクを実行する。 つまり、ワーカースレッドとの通信が必要になる。 |
QObjectクラスを継承したクラスを作成して、必要なスロットとシグナルを実装する。 |
永久 | オブジェクトを別のスレッドに常駐させて、ポートのポーリングや、 GUIスレッドと通信できるタスク等を繰り返し実行する。 |
上記と同様であるが、ワーカースレッドにてタイマを使用してポーリングを実装する。 |
スレッドの作成(非推奨)
以下に示す方法は、スレッドに低機能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
優先順位の逆転の問題がある場合、 優先順位の逆転を参照すること。