21
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

QtAdvent Calendar 2017

Day 4

Qtのマルチスレッド機構

Posted at

 今年のカレンダーは皆さんあげるのが早くてプレッシャーをかけられていたhermit4@おやつ部です。

 昨日は@zattu1さんによる「各OSのデプロイ方法」でした。最近ではデプロイツールが用意されているものの、それだけでは色々不足していたり、マーケットに出すためのHow-toが難しかったりと、デプロイはわりと敷居が高いイメージです。過去のカレンダーでマーケットに出すにあたっての苦労に触れた記事もあったかと思いますので、探して見て下さい。

 さて、Qtのカレンダーも4年目ですが、毎年スレッド関連をちらほらと書かせていただいています。

 以前の記事の紹介の中ではうっかり失念していましたが、一個あまり参照されていない記事の中に、QExceptionをとりあげていました。これは、QtConcurrent(map,filter,reduce)のスレッドから呼び元のスレッドにthrowできる特別な実装をされたexceptionです。

 「スレッドの同期について学ぼう(その2)」は、昨年、記事を書きたいので開けてほしいという依頼があったので止めたままでした。ごめんなさい。現在は、別件で書籍のレビュー中でして、カレンダー向けの記事の動作検証にあまり時間を割けなかったので、その2についてはまたの機会ということで。

 実は、本当なら過去の記事を取りまとめ、同期や終了処理などを色々追記してこの冬のコミケでマルチスレッド入門を手がける予定だったのですが、残念ながら落選したので、まずは、まだ触れていないスレッド機構も含め、どんなスレッド機構があるのか、どう使い分けるのかについて整理しておきたいと思います。

参考文献

Qtのマルチスレッド機構

 Qtのマルチスレッド機構には、コレまでに紹介したQThread, QtConcurrent(map,filter,reduce)を含め、下記のような種類があります。

  • QThread
    低レベルAPI
  • QtConcurrent(map,filter,reduce)
    高レベルAPI
  • QThreadPool/QRunnable
    再利用可能なスレッドのコレクション
  • QtConcurrent::run()
    関数の別スレッド呼び出し
  • WorkerScript
    QML上の処理の別スレッド実行

 スレッドを実行するために複数の手段が提供されているのは、言語やカスタマイズ性、利便性が異なる手段を提供しているためです。

機能の比較

機能 QThread QRunnable and QThreadPool QtConcurrent::run() QtConcurrent ( map , filter, reduce) WorkerScript
言語 C++ C++ C++ C++ QML
プライオリティ設定
イベントループの実行
シグナル経由のデータ受信
シグナルを使ったコントロール
QFuture経由のモニタ △ (限定的)
pause/resume/cancel

言語

 言語ですが、C++かQMLかの違いがあります。
 QMLでは、GUIスレッドと並列動作するJavaScriptをWorkerScriptを使って実行することができます。WorkerScriptは受け渡せるデータの種類が限定的ですが、ListModelを利用できるため、QML内で利用するには十分利用でき、実装も容易です。もちろん、シグナル経由でC++側で別スレッドに移したQObjectとデータの受け渡しも可能ですので、スレッドで行う処理はC++で実装するという手段もありますが、WorkerScriptに比べると実装は複雑になります。

優先度

 スレッドの実行優先度を決めるプライオリティを設定できるのは、QThreadとQThreadPoolのどちらかだけとなります。

  • void QThread::setPriority(Priority priority)
  • void QThreadPool::start(QRunnable *runnable, int priority = 0)

イベントループの実行

 起動したスレッド内部でイベントループを実行できるのはQThreadだけとなります。「Qtでスレッドを使う前に知っておこう」で解説したように、スレッド間のスロット受信にはイベントループが必須です。スロットを利用するようなQtクラスをはじめとする複雑な処理については、QThreadでの実装が必須となります。

シグナルとスロット経由のデータ受信

 この処理にはイベントループが必要なため、利用できるのはQThreadと、シグナルとスロットの機構をJS Engine側に委ねているWorkerScriptの2種類になります。

シグナルを使ったコントロール

 スレッドのコントロールにシグナルを使えるかということです。QtConcurrent(map, filter, reduce)は、QFutureWatcherを使いコントロールできます。

 QThreadとQFutureWatcherには以下のコントロール用slotが用意されています。

  • void QThread::quit()
  • void QThread::start(Priority priority = InheritPriority)
  • void QThread::terminate()
  • void QFutureWatcher::cancel()
  • void QFutureWatcher::pause()
  • void QFutureWatcher::resume()
  • void QFutureWatcher::setPaused(bool paused)
  • void QFutureWatcher::togglePaused()

QFuture経由のモニタ

 QFutureは、QtConcurrentのスレッド・結果の処理機構です。QtConcurrentのcancel, pause, resumeといった機能と、同期と結果の取得を行う事ができます。ただし、QtConcurrent::run()は、同期と結果の取得のみで、cancel、pause、resumeといった機能は利用できません。
 前述のQFutureWatcherクラスは、このQFuture機能をシグナルとスロットで処理できるようにするためのクラスです。

cancel, pause, resume

 すべてのスレッドが対象ではないですが、QtConcurrent(map, filter, reduce)について、cancelやpause、resumeといった機能がビルトインとして提供されています。
 その他のスレッド機構でこれらの機能が必要な場合、自分でこれらを用意する必要が生じます。

各スレッド機構の特色

QThread

 QThreadは、スレッド機構の基盤として低レベルAPIを提供するクラスです。QThreadのインスタンスは、1つのスレッドを管理します。注意事項としては、QThread自体は、スレッドを起動する側のイベントループに所属します。

 QThreadには、サブクラスを作成する手法(旧来の使い方)と、直接インスタンスを作成して、サブスレッドからQObjectスロットを呼び出す手法(最近の使い方)とがあります。これについては、「Qtでスレッドを使う前に知っておこう」をご一読下さい。

 低レベルAPIですので、利用するためには十分な知識を必要とします。またスレッドを操作するのには色々と手がかかりますが、その代わり柔軟な実装が可能です。

QThreadPool と QRunnable

 スレッドの生成と破棄は、基本的にコストが高い処理だと言われています。このため、並列処理をしたいというだけで、毎回スレッドを用意するのはパフォーマンス上許容できない場合があります。このような時に利用できるのがQThreadPoolです。QThreadPoolは、事前にスレッドを生成したまま保持しておき、QRunnableのサブクラスでQRunnable::runにスレッドで実行させたい処理を用意しておき、QThreadPoolにインスタンスを渡してstartすれば、スレッドプールから空いているスレッドが割り当てられ、その中でQRunnable::runが実行されます。実行が終わったら、スレッドは破棄されず、スレッドプールに戻されます。

 進捗状況の確認などはQRunnableのサブクラスを通じて共有することになりますが、このためにはミューテックスやセマフォなどを使って適切に処理する必要があります。

 注意事項としては、スレッドプールは事前に割り当てたスレッドを共用するため、あまりにコストの高い処理を走らせたり、プールされているスレッド数に対し大量のスレッドを実行した場合、パフォーマンスに影響が生じます。逆にほとんど利用しないのに大量のスレッドを起こすと、起動時の時間や、メモリ使用量に影響を与えます。このあたりは必要に応じてチューニングしましょう。

 なお、Qtのアプリケーションは、CPUのコア数に基づき最適な数のスレッドプールをグローバルスレッドプールとして作成しています。QThreadPool::globalInstanceでアクセス可能です。グローバルスレッドプールは、QtConcurrent::runでもデフォルトで利用されたり、他のQtクラスでも利用しているかもしれません。あらかじめ長時間処理するスレッドや、大量のスレッドを必要とする処理であるとわかっている場合は、独自にスレッドプールを用意し、プライオリティを下げて呼び出す等の工夫が必要になります。

QtConcurrent::run()

 QThreadPoolはQRunnableのサブクラスを作成しなくてはなりません。また、終了確認や結果の取得についても、独自に実装する必要があります。面倒だなと思った時に、面倒を避けるの努力をするのがQtです。というわけで、関数を渡せば、別スレッドで実行してくれるAPIが、QtConcurrent::runです。デフォルトではグローバルスレッドプールが利用されますが、これは先の注意事項と同様で、利用するスレッドプールを指定するAPIも用意されています。

 スレッドの実行状態と結果は、QFutureを使って取得可能ですが、cancel/pause/resumeやprogressといった機能は利用できませんのでご注意下さい。進捗表示などが必要な場合には、スレッドセーフな変数を用意するなどの手間がかかります。

QtConcurrent (map, filter, reduce)

 Qtの並列処理の高レベルAPIで、いくつかの一般的な並列計算のパターンを扱う機能を提供しています。
 これらは、同期、進捗通知、中断、一時停止、レジューム処理がビルトインで提供されており、ミューテックスやセマフォなどでスレッドセーフを用意する必要がありません。
 これらの機能は、使用可能なすべてのプロセッサコアに自動的に割り当てるため、実装を変える事無くハードウェアの変更に伴いスレッド数が制御されます。
 利用法などは、「QtConcurrentでマルチスレッドに挑戦」を参照して下さい。

WorkerScript

 WorkerScriptはQML typeとして提供されているQMLのためのスレッド機構です。
 WorkerScriptインスタンスには、1つの.jsスクリプトをアタッチすることができ、sendMessageを受け取ると実行が開始されます。処理が終了するとGUIスレッドに返信してWorkerScript::onMessage ハンドラを呼び出します。
 データはシグナルを介してスレッド間で転送されますが、転送可能な型には制限があります。  

  • ブール値、数値、文字列
  • JavaScriptオブジェクトと配列
  • ListModel オブジェクト

 これ以外の型の受け渡しはできないので、注意が必要です。

ユースケースの例

どうもいまいちな感じなのですが、ドキュメントの記載を大雑把に翻訳して転載しておきます。

実際には、どこまで複雑な処理を必要とするのかを基準に判断するのが良いでしょう。他スレッドとのやりとりが必要なのか、同期や進捗表示は必要かなどを勘案し、一番使いやすい方法を採用するのが近道かと思います。

スレッドの寿命 目的 解決策
短期的 新しい線形関数を別のスレッド内で実行する。オプションで、実行中に進行状況を表示する 実装手段は複数あります。
  • QThreadのサブクラスを作りQThread::run()を再実装し、QThreadでスレッドを起動する。進捗表示はシグナルを使って通知する
  • QRunnableのサブクラスを作りQRunnable::run()を再実装し、QThreadPoolでプールされたスレッドを割り当てる。進捗表示はシグナルを使って通知する
  • 関数に対しQtConcurrent::run()を使ってプールされたスレッドを割り当てる。進捗表示はスレッドセーフな変数を使って通知する
短期的 別のスレッド内で既存の関数を実行し、その戻り値を取得する 関数に対しQtConcurrent::run()を使ってプールされたスレッドを割り当てる。処理が終了するとQFutureWatcherがfinishedシグナルを出すので、QFutureWatcher::result() を使い結果を取得する
短期的 CPUのコアを駆使し、コンテナのアイテムに対して操作を実行する コンテナの対象アイテム選択にはQtConcurrent::filter()を、QtConcurrent::map()で操作を実行する。結果を単一の結果に変換する場合はQtConcurrent::filteredReduced() やQtConcurrent::mapeddReduced()を利用する
短期的 / 長期的 QMLアプリケーションでコストの高い計算処理を行い、終了したらGUIを更新する 計算コードを.jsスクリプトに置き、それをWorkerScriptインスタンスにアタッチする。 sendMessage()を呼び出し、新しいスレッドで計算を開始する。 結果は、WorkerScript::onMessage で受け取り、GUIを更新する
恒久的 要求に応じて異なるタスクを実行することができ、新しいデータを受け取ることができる別スレッド上にオブジェクトを存続させる QObjectをサブクラス化してWorkerを作成する。このWorkerオブジェクトとQThreadをインスタンス化し、workerを、moveToThreadを使い新しいスレッドに移動する。待機中のworkerへは、シグナル・スロットを使いコマンドまたはデータを送信する
恒久的 スレッドがシグナルやイベントを受け取る必要がない別のスレッドで、負荷の高い操作を繰り返し実行する QThreadのサブクラスを作成し、QThread::run()を再実装する。run()の中ではイベントループなしで単純なループで処理を行い、スレッドからデータをGUIスレッドに返す場合は、シグナルを送信して通知する

Qtでスレッドを使う前に知っておこう」でも説明したとおり、イベントループが無くても、シグナルの送出は可能です。イベントループは、イベントの確認、配送、シグナルの受信などの処理があるため、負荷の高いスレッドで必要も無いのに動かすのは好ましくありません。

他方、スレッド間で頻繁にデータの受け渡しが想定される場合、スレッドセーフな変数を用意するのは実装に手間がかかるため、シグナルとスロットを使う方が便利です。BlockingQueuedなシグナルを使えば、簡易的に同期処理を行う事も可能です。

スレッド関連の一覧

実は、Qtのクラス群の中でも、スレッド系の数はそこまで多くはありません。
これまでにおおよその説明はしてきており、同期(その2)でQFuture/QFutureWatcherについてもう少し説明と、QReadWriteLockerについて解説が必要かなと思っているのと、QThreadStorageの説明が抜けてたなって程度です。1500以上のクラスがあるQtの中で、複数の機構とはいえ、たったこれだけのクラスを調べて見れば良いのです。たいした事無い・・・ですよね。

Qtドキュメント - スレッド関連

Qtの難点の一つが、ドキュメントの中から必要な情報を書いてあるページを見つけにくい・・・・と思っているのですが、どうでしょう。とりあえず、スレッド関連は以下の通りです。他にも見つけてないドキュメントがあるかもしれませんが・・・。

まとめ

今回は、どちらかというと自分自身のために、こんなのもあったなと思い出すための記事として書き殴ってみました。かなりざっくりとした内容でごめんなさい。

CPUのコア数が増え、マルチスレッドの重要度は増してきています。マルチスレッドプログラミングは、やっかいな不具合を引き起こしたり、同期などの難しい事を考えなくてはならなかったり、面倒が多いため避けてしまうという話を聞くことがありますが、Qtはやっかいさを少しでも簡単にできるような仕組みが整えられています。

スレッドで動かすという目的に対し複数の手段が用意されてしまっているので、はじめはちょっと面食らうかもしれませんが、それぞれの特徴を知っていれば、どれを使うかはそれほど迷うこともないでしょう。この記事が、わずかでも、スレッド機構の特徴をつかむお手伝いができれば幸いです。この記事が、あなたの楽しいプログラミングライフの一助となりますように。

明日は、まだ参加者がおりません。どなたかお時間か知識に余裕があるかたは、是非是非ご投稿下さい。

21
16
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
21
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?