RunLoop
RunLoop とは
RunLoop とは、あるスレッドにおいて入力イベントに応答するためのループ。所謂 イベントループ パターンを実装したもの。入力イベントとそれに応じて実行する処理を設定しておくと、入力イベント発生時に登録した処理を駆動してくれる。 Run loops are part of the fundamental infrastructure associated with threads. A run loop is an event processing loop that you use to schedule work and coordinate the receipt of incoming events. The purpose of a run loop is to keep your thread busy when there is work to do and put your thread to sleep when there is none.
The programmatic interface to objects that manage input sources.
スレッドセーフティ
RunLoop オブジェクトは スレッドセーフではない ので注意する。同一スレッドからのみ触らないと予期しない動作に陥る可能性がある。 The RunLoop class is generally not thread-safe, and you must call its methods only within the context of the current thread. Don’t call the methods of a RunLoop object running in a different thread, which might cause unexpected results.
RunLoop への入力ソースのアタッチ
code:swift
let runloop = RunLoop.current
// タイマーを登録する
// 時刻をループ内で監視して、タイマー側に設定された処理を発火する
let timer = // ...
runloop.add(timer, forMode: .default)
// Portを登録する
// Q. Portって?
// A. よく知りません。スレッドやタスク間でやりとりするための何某らしいです
let port = // ...
runloop.add(port, forMode: .default)
// selectorをスケジュールする
// スケジュールされたselectorはループ内で直列に実行されるらしい (Threading Programming Guideより)
let performer = // ...
runloop.perform(#selector(performer.doSomething),
target: performer,
argument: nil,
order: 0,
// ループ内で一度だけ実行するブロックをスケジュールできる
// スケジュール後の次のループで発火される
runloop.perform {
// 何か処理
}
// Combineのスケジューラとしても利用できる
// 下記の場合、sinkに渡されたブロックはループにスケジュールされ、ループ内で発火されることになる
_ = Just(1)
.receive(on: runloop)
.sink { /* ... */ }
RunLoop Modes
RunLoop は実行時に モード を指定することができる。RunLoop はどれか 1 つのモードで実行される。あるモードでの実行中は、そのモードに紐づけられた入力ソースのみが監視される。あるモードで実行中に、他のモードに紐づけられた入力ソースからイベントが飛んできた場合、そのイベントは対応するモードに切り替わるまで保留される。 モードには以下の種類がある。
Default: デフォルトのモード
Modal: モーダルパネルを対象としたイベントを処理する専用のモード
詳しくはよく分からなかった
Event tracking: タッチやスワイプ、スクロール等のUIイベントを処理する専用のモード
Common: 複数モードを含んでいる汎用モード
iOS の場合、初期値だと Default, Modal, Event Tracking を含むらしい
例えば iOS アプリの場合、RunLoop.main は通常時は Default モードで動作しているけれど、ユーザのタッチ操作を受け付けると Event Tracking モードに切り替わる。 このモードにまつわる有名な問題として、ユーザのスクロールしている最中にタイマーが一時停止してしまう問題 がある。タイマーを Default モードで RunLoop に紐づけていた場合、タイマーのイベントは Default モード中しか発火されない。 code:swift
let timer = // ...
RunLoop.main.add(timer, forMode: .default)
ユーザがスクロールを始めると、スクロールを行っている最中は RunLoop が Event Tracking モードに切り替わってしまうため、Default モードにアタッチされたタイマーのイベントが発火されない。タイマー経由で UI を更新していた場合、UI の更新が止まってしまう。 これを避けるためには、Default と Event Tracking の両方のモードを兼ね備えた Common モードにアタッチしておく。これにより、ユーザがスクロールを始めたことで Event Tracking モードに切り替わっても、タイマーのイベントが発火され続ける。
code:swift
let time = // ...
RunLoop.main.add(timer, forMode: .commo)
RunLoop のライフサイクル
RunLoop の開始
メインスレッド以外に紐づけられた RunLoop は明示的に開始する必要がある (RunLoop.main のループはアプリ実行中は常に実行されているらしい)。ただし、開始した際に入力ソースがアタッチされていない場合は即座に終了してしまう。 Starting the run loop is necessary only for the secondary threads in your application. A run loop must have at least one input source or timer to monitor. If one is not attached, the run loop exits immediately.
RunLoop の開始には run() が利用できる。これは実行したタイミングで実行元が RunLoop の無限ループにアタッチされ、RunLoop 終了までそこで処理がブロックされる。が、下記のように入力ソースをアタッチしていない状態で実行しても、即座に実行が終了して処理が次に進んでしまう。 code:swift
DispatchQueue.global().sync {
// バックグラウンドスレッドに紐づいたRunLoopを生成
let loop = RunLoop.current
// RunLoopが開始されるが、何もアタッチされていないので即座に終了する
loop.run()
}
また、入力ソースがアタッチされても、RunLoop が開始されるまでは実行されない。 code:swift
DispatchQueue.global().sync {
let loop = RunLoop.current
// 実行されない
loop.perform {
// 何か処理
}
}
以下のように記載することで、perform に私が closure が実行され、実行後にブロックが解除される。
code:swift
DispatchQueue.global().sync {
let loop = RunLoop.current
loop.perform {
// 何か処理
}
loop.run()
}
RunLoop.main.run()
メインスレッドに紐づけられた RunLoop の run() メソッドは、下記のように、Swift スクリプトで非同期処理の実行を待つためにも利用される。 code:script.swift
// 何か非同期な処理
someAsyncTask {
// 何か処理
exit(EXIT_SUCCESS)
}
RunLoop.main.run()
RunLoop.main はメインスレッド用の RunLoop であり、基本的にアプリケーション実行中はループが動き続けている。ので、RunLoop.main.run() を呼び出した時点でアプリケーション終了まで処理の実行がその行でブロックされる。 exit() メソッドは現在のスレッドを終了させることができるので、メインスレッドから呼び出すとメインスレッドを終了させ、同時に RunLoop.main を終了させ、待ち合わせを解除することができ、結果的に非同期処理を待ってからスクリプトを終了することができる、という仕組みになっている。 RunLoop の終了
RunLoop に紐づけられた入力ソースが全て取り外されたらループは終了する可能性があるけど、確実ではないらしい。 If no input sources or timers are attached to the run loop, this method exits immediately; otherwise, it runs the receiver in the NSDefaultRunLoopMode by repeatedly invoking run(mode:before:).
...
Manually removing all known input sources and timers from the run loop is not a guarantee that the run loop will exit.
Although removing a run loop’s input sources and timers may also cause the run loop to exit, this is not a reliable way to stop a run loop. Some system routines add input sources to a run loop to handle needed events. Because your code might not be aware of these input sources, it would be unable to remove them, which would prevent the run loop from exiting.
RunLoop.main と DispatchQueue.main との違いは?
DispatchQueue.main は、RunLoop にアタッチされた入力ソースとは並列に実行されるらしい。ので、RunLoop にアタッチされた他の入力ソースの影響は受けない。RunLoop の実行モードも関係ない。 The main dispatch queue is a globally available serial queue that executes tasks on the application’s main thread. This queue works with the application’s run loop (if one is present) to interleave the execution of queued tasks with the execution of other event sources attached to the run loop. Because it runs on your application’s main thread, the main queue is often used as a key synchronization point for an application.
一方で、RunLoop は紐づけた処理の実行モードによってはイベントが即座に発火されないことがあるので注意する必要がある。例えば Combine のスケジューラとして RunLoop を利用する場合は、RunLoop の Default モードにしか処理を登録できない。そのため、ユーザのスクロール操作により Event Tracking モードに切り替わっている最中は、ストリーム内の処理が実行されなくなってしまう。Common モードで処理をアタッチできると良いのだけど、現状そのような口はない。 code:swift
somePublisher
.subscribe(in: RunLoop.main)
.sink { /* Default モード中しか実行されない */ }
参考