マルチスレッド基礎編
macOS Mojava 10.15.7 / Xcode 11.3.1 / Swift 5.0
概要
ひとつのアプリケーションの中で、複数のタスクを同時に並行して実行することを一般的にはマルチタスク処理という。実現する方法は、システムにより色々だが、Cocoaフレームワークでは、これををマルチスレッド方式により実現している。
タスクは、実体はソースコードであるが、意味としては、一貫性のあるひとつのまとまった仕事といったものだろうか。メソッドや関数は最小単位タスクであるが、実際はも少し大きな処理のかたまりとなるだろう。
タスクはスレッドの上で動く。スレッドはシステム資源の一部である。スレッドは、その上でタスクが動く「舞台」に喩えることができる。タスクは開始から終了までスレッドを占有するので、複数のタスクが処理を待っている場合、前のタスクが終了してから、次のタスクが起動する。
イベントドリブン型のアプリケーションは、イベントの発生を待ち、イベントが発生したら受け付け、イベントハンドラを起動する。イベントハンドラは所定の処理を行い、多くの場合、結果を UIコントロールに出力する。この処理のサイクルをイベントループと呼ぶ。
ウィンドウアプリケーションは、既定では、メインスレッドをひとつだけ持ち、ここでイベントループに関わる全ての処理を行う。一般的にシングルスレッド方式という。
イベント処理はメインスレッド上で逐次に行われるので、例えば、あるイベントの処理が、ストリームの入力を伴うような処理時間が長いものであれば、それが終わるまで、次のイベント処理は待ち状態になる。スレッドがひとつしかないので、これは避けられないことである。
マルチスレッド方式の採用
一連のイベント処理から、イベントハンドラ本体を別タスクとして切り出すことができる。マルチスレッド方式を採用すれば、メインスレッドはイベントの受け付けとイベントハンドラの起動だけを行い、時間のかかるイベントハンドラ本体は、別スレッドを作成し、そこで実行させることができる。こうすることで、処理時間の長いイベントハンドラの実行中でも、次のイベントを受け付けることができる。
マルチスレッド方式の適用例としては、キーワードによりファイルを全文検索する を参考にされたい。
ここでは、処理に時間がかかるテキスト全文検索処理を別スレッドで実行し、メインスレッドはイベント受け付け可能になっている。これによりユーザの指示(ボタンのクリック)により、検索処理を途中でキャンセルできる。
また、テキスト全文検索処理を10個のタスクに分轄して、並列に実行し、全体の処理時間の短縮を図っている。時間のかかるバッチ処理を並列化して実行することもマルチスレッド方式により可能となる。
処理方式
Swiftにおいてマルチスレッド処理を実装するには、Grand Central Dispatch(GCD)機能を内包した DispatchQueueクラスを利用する。マルチスレッドの処理方式には重要な二つのポイントがある。
(1) 同期処理と非同期処理
メインスレッドから別スレッドを作成し処理を起動したとき、メインスレッドが別スレッドの処理が終了するまで待つのが同期処理、待たないで即座に次のコードに進むのが非同期処理である。
(2) タスクの起動形態
キューに複数のタスクを投入したときの起動形態である。DispatchQueueクラスの Attributes属性で指定する。
順次起動:initiallyInactiveを指定すれば複数のタスクはキューに投入された順序に従って、前のタスクが終了してから次のタスクが始まるというように、順次に起動する。スレッドには常にひとつのタスクだけが動く。
同時起動:concurrentを指定すればキューに投入された複数のタスクは即時に起動する。スレッドには全てのタスクが同時に並列してに動く。
実装の基本パターン
アプリケーションは、キューオブジェクト(DispatchQueueクラス)を取得し、処理を記述したクロージャを引数に指定した asyncメソッドまたは syncメソッドを呼ぶ。あとはシステムが自動的にスレッドを作成し、処理を実行する。
キューの種類
(1) グローバルキュー
システムがひとつだけ持つキューで、全てのアプリケーションが利用可能である。タスクの起動形態は順次起動(concurrent)である。
(2) カスタムキュー
アプリケーション内で使用する独自のキューを作成する。タスクの起動形態を attributes引数に指定することで、順次起動/同時起動いずれのキューを作成することができる。labelの文字列は、何でもよいが、逆順ドメインのバンドルIDを指定しておくことが多い。
(3) メインスレッドキュー
メインスレッドでタスクを実行ためのキュー。上述のキューを経由して別スレッドで動く処理が UIコントロールにアクセスするときに使用する。単独で使うことはほとんどない。タスクの起動形態はは順次(initiallyInactive)である。
実装例
(1) 非同期処理
キューを取得し asyncメソッドを呼べば、別スレッドで動く非同期のタスクが起動する。メインスレッドの print("終了") は、起動したタスクの print("Hello") を待たないで、すぐに実行される。
(2) 同期処理
キューを取得し syncメソッドを呼べば、終了を同期するタスクが起動する。メインスレッドの print("終了") は、起動したタスクの print("Hello") が終わるまで実行されない。
(3) タスクの起動形態:同時起動
属性が同時起動(concurrent)のキューを取得する。5個のタスクを投入すると、それぞれ別スレッドで同時に稼働する。終了は順不同である。
(4) タスクの起動形態:順次起動
属性が順次起動(initiallyInactive)のキューを取得する。5個のタスクを投入すると、投入した順に起動する。稼働するスレッドはひとつだけで、前の処理が終わってから次の処理が始まる。initiallyInactive属性のキューは、activateメソッドで有効化しないと実行時にエラーとなる。
(5) メインスレッドの使い方
フレームワークの仕様では、テキストフィールドなど UIコントロールへのアクセスは、メインスレッドで行わなければならない。別スレッドからアクセスするとアプリケーションは異常終了する。
別スレッドのタスクが、UIコントロールへアクセスするには、その処理だけメインスレッドのタスクで行う。次の例は、別スレッドで動いているタスクが、メインスレッドキューを取得して、asyncメソッドを呼び、テキストフィールドの値を更新している。
メインスレッドからメインスレッドに処理を投入することもできる。ただし実行順序が変わるだけで、処理自体は全てメインスレッド上でシリアルに動くので、あまり意味はない。async(非同期)メソッドは動くが、sync(同期)メソッドはなぜか実行時エラーを起こす。
セマフォによる処理の同期
セマフォを使えば、別スレッドで動く処理と終了同期をとることができる。機能としては、同期スレッド処理(syncメソッド)と似たようなものであるが、どのようなケースで使うのか?
一例として、メインスレッドから二つののタスク同時に起動し、並列で処理し、全て終了するまで待つとする。
syncメソッドを使った同期処理にすると、最初のタスクが終わるまで次のタスクは開始しないので並列処理にならない。asyncメソッドを使った非同期処理にすると、並列で処理されるが、メインスレッドでタスクの終了を待つことができない。
DispatchSemaphoreクラスのセマフォを使えば、メインスレッドは非同期に起動した処理の終了を特定の地点で待つことがでる。次に、セマフォを使って二つのタスクの終了を待つコードを示す。
セマフォオブジェクトの valueプロパティは終了カウンターの役目を果たす。waitメソッドを呼ぶと valueプロパティの値が 1増え、そこで処理が止まる。signalメソッドを呼ぶと 値を1減らす。値がゼロになったとき処理が再開する。ここでは二つのタスクの終了を監視するので、waitメソッドを 2回呼び、valueプロパティの値を 2にしておく。各タスクは終了時にそれぞれ signalメソッドを呼ぶ。