はじめに
TCAのPerformanceに関するドキュメント内で、Task.yield()
というAPIを使用したCPU負荷対策の例が挙げられていました。
今までTask.yield()
を使ったことが無く、ドキュメントを読んでも何をしているのかいまいちわからなかったので調べてみました。
Task.yield
Task.yield()
は現在のタスクをサスペンドし、同じスレッドで別のタスクを実行できるようにするためのAPIです。
以下のコードを見てください。
最初に無限ループするタスクを100個生成し、その後「Hello, world!」と出力するタスクを生成します。
プログラムの実行が終了しないように、最後にwhile true {}
でメインスレッドをブロックしています。
for _ in 0..<100 {
Task {
while true {}
}
}
Task {
print("Hello, world!")
}
while true {}
このコードを実行すると、「Hello, world!」は出力されません。
無限ループするタスクが全てのスレッドをブロックしていて、「Hello, world!」を出力するタスクはスレッドが空くのをずっと待ってしまっています。
Xcodeで実行を一時停止してみると、10個のスレッドが無限ループによってブロックされていることがわかります。
そこでTask.yield()
の出番です。
無限ループ内でTask.yield()
を実行するようにしてみましょう。
for _ in 0..<100 {
Task {
while true {
await Task.yield() // 追加
}
}
}
Task {
print("Hello, world!")
}
while true {}
すると、「Hello, world!」が出力されました。
Task.yield()
によって無限ループが一時的に中断され、「Hello, world!」を出力するタスクにスレッドが割り当てられたのです。
出力が完了したらそのタスクは終了するので、スレッドはまた無限ループの実行に戻ります。
より実用的なケースでは、実行に長い時間がかかる計算処理の途中にTask.yield()
を入れておくことで、別の並行タスクの実行が遅くなってしまうのを防ぐことができるというわけです。
ReducerでCPU負荷の高い処理を効率的に実行する
Task.yield()
が理解できたところでTCAのドキュメントを読んでみます。
Reducerはメインスレッドで実行されるため、Reducerで直接CPU負荷の高い処理を実行しようとすると、UI操作が重くなるなどの影響が出るため避けるべきです。
case .buttonTapped:
var result = // ...
for value in someLargeCollection {
// Some intense computation with value
}
state.result = result
そのような処理はEffectを使い、非同期に実行されるようにします。
また、長い時間スレッドをブロックしてしまうような処理を行う必要がある場合はTask.yield()
を呼び出し、適度な間隔でスレッドを解放してあげましょう。
以下の例では、1000回のループごとに1回Task.yield()
を呼び出しています。
case .buttonTapped:
return .run { send in
var result = // ...
for (index, value) in someLargeCollection.enumerated() {
// Some intense computation with value
// Yield every once in awhile to cooperate in the thread pool.
if index.isMultiple(of: 1_000) {
await Task.yield()
}
}
await send(.computationResponse(result))
}
case let .computationResponse(result):
state.result = result
このようにすることで、メインスレッドをブロックせず、他のEffectの実行にも影響を与えずに、CPU負荷の高い処理を効率的に実行することができます。