Swift ConcurrencyのTaskを理解する
はじめに
Swift Concurrency周りの処理を調べたときに大体APIを叩く時のasync/await周りの紹介記事ばかりでTaskに関してちゃんとまとめられている記事がなく、雰囲気でTaskを使っていたので調べてみました
Task
以下の検証用のメソッドを使ってTaskの挙動を見ていきます、1秒後にVoidを返すシンプルなメソッドです
func fetchHogeAPI() async throws -> Void {
try await Task.sleep(nanoseconds: 1 * 1000 * 1000 * 1000)
return ()
}
そのまま書いた場合
そのままAPIを叩くメソッドを叩いた場合以下のようなエラーが表示されます、同期メソッドがからasyncメソッドを呼び出すことはできずエラーになってしまします。
do {
try await fetchHogeAPI()
} catch {
print(error)
}
'async' call in a function that does not support concurrency
Taskを使う
定番のやり方としてTask.initを使ってやるとコンパイルエラーが消えてビルドが通るようになります。
.initは省略可能なのでTask{...}
と書くことができます。
Task {
do {
try await fetchHogeAPI()
} catch {
print(error)
}
print("call2")
}
なぜTask.initで囲うとエラーが出なくなるのかですが、クロージャにはasyncがついているのでTaskのスコープの中ではawaitができると言うことになります。
init(priority: TaskPriority?, operation: () async -> Success)
エラーについて
Taskはerrorも吸収してしまいます、以下のようにdo{}catch{}しなくてもコンパイルエラーは出ずにビルドが通ってしまいますが、エラーを表示させたい場合はdo{}catch{}でエラーが起きたことをハンドリングする必要があります。
Task {
try await fetchHogeAPI()
}
メインスレッドで処理を行う
メインスレッドで処理を行いたいケースもあると思いますが、以下どちらかを使う必要があります。
Taskの中で重い処理(受け取った値をファイルに書き込みなど)がある場合は一部をメインスレッドで行うのが良いと思います
// Taskの中全てをメインスレッドで行う
Task { @MainActor
}
// Taskの中の一部をメインスレッドで行う
Task {
await MainActor.run{
}
}
//❌これは@Sendableじゃないのでやめておいた方が良い
DispatchQueue.main.asyncAfter{
}
スレッドをブロックしてしまうケースについて
以下のコードのように通信が終わった後に重い処理(ファイルへの書き込みなど)が発生する場合はそのままだとメインスレッドをブロックしてしまうので画面が固まります。
Task {
do {
try await fetchHogeAPI()
// 重い処理
} catch {
print(error)
}
}
その場合は、Task.detachedを使って重い処理の部分を別スレッドに逃してあげることで、スレッドがブロックされるのを防ぐことができます
Task {
do {
try await fetchHogeAPI()
Task.detached {
// 重い処理
}
} catch {
print(error)
}
}
重い処理を待ってから次に進みたいとき
思い処理が終わった後に処理を継続させたいケースでは、.valueで値を取り出そうとするとTaskをawaitできるようになるので処理を待つことができる
Task {
do {
try await fetchHogeAPI()
_ = try await Task.detached {
// 重い処理
}.value
} catch {
print(error)
}
}
Sendable
並行に扱っても安全な型
- actor協会を超えるのに必要(actorのメソッドに引数として渡したり戻り値として受け取ったりする時)
- Task.initなど@Sendableクロージャにキャプチャするのに必要