こんにちは。田原です。

前回はSignal/Slotのごく基本であるコールバック関数的な使い方を解説しました。双方向依存を断ち切るのに有用な方法でしたね。しかしSignal/Slotは更に強力です。(強力過ぎて頭痛い程)それはお手軽だけど実は本格的にスレッド間通信できるからです。Qtのスレッド間通信(というか非同期プログラミング)の要と言っても過言ではないと思います。今回はその基本部分を解説します。その後2回ほどかけて深堀りしていきます。

1.Signalはキューに並ぶ(ことがある)

1-1. 2つの仕組みがある

Signalが発行されたらSlotが呼ばれます。これだけなら、普通は「①Signalを発行(emit)するとその場から直接Slot関数が呼ばれる」と理解するのではないでしょうか?
そのように振る舞うこともありますが、そうではないこともあります。「②Signalを発行(emit)するとキューへエンキューされ、それをデキューしてSlot関数が呼び出される」という仕組みも存在しているのです。

前者のみを使う分にはコールバック関数を登録する仕組みとして理解しておけば十分です。
しかし、それは勿体ないです。マルチスレッドでプログラム作成する時に後者の機能が超便利なのです。スレッド間で関数呼び出しを排他制御に煩わされることなく行えるというマルチスレッド・プログラム開発時には非常にありがたい仕組みなのです。(逆に、シングル・スレッドで十分な場合はあまり有り難みを感じることはないかも知れません。)

1-2. スレッド間通信としての応用

マルチスレッド・プログラミングする際、各スレッドが完全に独立して動作することはほとんどなく、多くのケースではスレッド間での通信が必要になります。その方法は多数あるのですが、キューを介して通信したいことがあります。

例えば、メイン・スレッドからサブ・スレッドへ要求し、結果をサブ・スレッドがメイン・スレッドへ返却するという流れは比較的よく使われると思います。
その際にメイン・スレッドからサブ・スレッドへ要求する場合にサブ・スレッド用のキューへエンキューし、逆も同様という方法は非常に面倒でコーディングとデバッグの手間がたいへんなのですが、比較的よく使われます。(手抜きしてもっと簡単な方法で開発を初めても、プロジェクトの進行に伴い、キューを使わないと解決できない問題が出てくることは少なくありません。)

Qtの場合は、そのようなケースに至る前に最初からキューを使っておけば良いのです。Signal/Slotを使うと既にキューが「裏」で実装されており、キュー周りのコーディングとデバッグは不要なのです。問題はその裏側の仕組みを把握してきちんと設計することです。(ここ2~3回でそのあたりまで含めて解説する予定です。)

2.具体的に

Sinal/Slotの関係は上述したように大きく2つあります。そして、更にキュー経由する方の仕組みは2つあります。

①Signal発行(emit)するとその場から直接Slot関数が呼ばれる
②Signalを発行(emit)するとキューへエンキューされ、それをデキューしてSlot関数が呼び出される
  ②-1)Signal発行時エンキューしたら直ぐに返ってくる(Slot関数の終了を待たない)
  ②-2)Signal発行時Slot関数の終了を待って返ってくる

①の場合、キューがないので当たり前ですが「エンキューしたら直ぐに返ってくる」は存在しません。常にSlot関数が終わってから返ってきます。

②-1)がよく使われます。リスクもほぼないのでできるだけこちらを使った方が良いです。
しかし、Signal発行側で確保したメモリ(多くはローカル変数でスタック上に確保するでしょう)に何かデータをセットしてSlot関数で処理させたいことが時々あります。その時にemitする際に当該メモリをコピーしてSlotへ渡せば②-1)で十分ですが、データ量が大きいような時コピーしたくないことがあります。そのような時は②-2)の仕組みを使って、Slot関数がメモリへのアクセスを終わってから呼び出し側がメモリを開放すれば良いです。
リスクはデッドロックです。要求元が②-2)で要求を出し、要求先がSlot関数内で②-2)で応答を返却しようとするとあっさりデッドロックします。意外に危険なのでなるべく②-2)は使わない、使う場合はデッドロックしないことを慎重に検討下さい。(例えば、要求元は必ず②-1)で要求し、要求先が②-2)で応答を返すことがあるなど。)

これらはconnect関数の最後のオプションで指定します。

意味 オプションの値
①直接呼び出し Qt::DirectConnection
②-1)キュー経由呼び出し Qt::QueuedConnection
②-2)キュー経由呼び出しSlot完了待ち Qt::BlockingQueuedConnection

前回解説したconnectの3種類全部について、それぞれ上記の3種類が存在するので合計9通りのSignal/Slotの接続が存在します。

3.サンプル・ソース

今回は単純なコンソール・アプリを作ってみました。
サブ・スレッドを1つ用意し、メイン・スレッド(デフォルトのスレッド=main()関数を呼び出すスレッド)からサブ・スレッドへstart要求を発行し、サブ・スレッドは3回の応答(direct, queued, blockingQueued)返却後、最後にfinished応答返却してメイン・スレッドはアプリを終了させています。

応答を受け取ったメイン・スレッド側は、それぞれのSlot関数で単純に1秒待ちしてSlot関数を終了しています。
サブ・スレッド側はSignal発行の前後で「現在時刻」を表示していますので、②-1)QueuedConnectionと②-2)BlockingQueuedConnectionの相違について時刻を見て振る舞いを把握してみて下さい。

今回のサンプルの主要部分のソースです。全体はGistにアップロードしていますので参考にして下さい。(Windowsでテストしています。)




実行結果
17:36:50.461 0x14f8 start SubThread()
17:36:50.465 0x14f8 end   SubThread()
17:36:50.466 0x14f8 start MainThread()
17:36:50.467 0x14f8 start start()
17:36:50.468 0x14f8 end   start()
17:36:50.469 0x14f8 end   MainThread()
17:36:50.468 0x567c start startSlot()
17:36:50.471 0x567c start directSlot()
17:36:51.472 0x567c start directSlot()
17:36:51.476 0x567c       startSlot(1)
17:36:51.477 0x567c       startSlot(2)
17:36:51.477 0x14f8 start queuedSlot()
17:36:52.479 0x14f8 end   queuedSlot()
17:36:52.480 0x14f8 start blockingQueuedSlot(): This is local variable in SubThread
17:36:53.482 0x14f8 end   blockingQueuedSlot(): This is local variable in SubThread
17:36:53.485 0x567c       startSlot(3)
17:36:53.485 0x567c end   startSlot()
17:36:53.485 0x14f8 start finished()
17:36:53.485 0x14f8 end   finished()
17:36:53.485 0x14f8 start ~SubThread()
17:36:53.485 0x14f8 end   ~SubThread()

4.まとめ

今回はQThreadと絡む部分の基本的な振る舞いを解説しました。
そして、実は「2.具体的に」で解説したオプションは後2つあります。そのうちの1つのQt::AutoConnectionが実は最もよく使われます。といいますか、このオプションはconnect関数の最後で指定するのですが省略可能で省略時はQt::AutoConnectionなのです。
これはQt::DirectConnectionとQt::QueuedConnectionを自動的に選びます。SignalとSlotが同じスレッドに属していたらQt::DirectConnection、そうでなければQt::QueuedConnectionです。簡単なようにみえますね。でも、実は非常に奥が深いです。
Slot関数が属するスレッドって何?って話です。普通は当該関数を呼び出したスレッドが「属するスレッド」です。その点、Signalをemitした時のスレッドがSignalが「属するスレッド」なのでSignalは簡単です。でもSlotは呼び出される前に「属するスレッド」が決定されます。何を基準に決めているのでしょう? 謎ですね。
この辺りがQtのSignal/Slotの難しい部分です。次回(もしかしたら次々回も含めて)に解説する予定ですのでしばしお待ち下さい!

それでは今回はこの辺で。お疲れさまでした。