Swift Concurrencyってなに?
Swift Concurrencyとは?
Swift ConcurrencyはSwift5.5から登場した非同期処理・並行処理をサポートする機能。今まで非同期処理というとクロージャーで実装することが多かったが、async/awaitを使うことで同期処理のように記述でき可読性が上がったり、actorを使うことでデータ競合を防ぐことができたりと、非同期処理・並行処理に関する処理が扱いやすくなった。
async/await
今まで非同期処理をクロージャーで実装することはよくあることで、例えばURLSessionでHTTPリクエストを行う場合は次のようになる。
// クロージャーの場合
func request(with url: URL, completion: @escaping (Result<Data, Error>) -> ()) {
let task = URLSession.shared.dataTask(with: url) { data, response, error in
// 処理が複雑な場合にcompletionを呼び忘れることも
completion(.success(data!))
}
task.resume()
}
// 呼び出し
request(with: URL(string: "<https://exmaple.com/path>")!) { result in
switch result {
case .success(let data):
// 成功時の処理
break
case .failure(let error):
// 失敗時の処理
break
}
}
上のコードをasync/awaitで書き直すと次のようになり、同期処理のように書けることがわかる。また、completionを忘れることもなく、エラーの場合はthrowできるので、より直感的に非同期処理を扱える。
// async/awaitの場合
// 非同期関数であることを示すためにasyncを付ける。エラーもthrowできる。
func request(with url: URL) async throws -> Data {
let (data, _) = try await URLSession.shared.data(from: url)
return data
}
// 呼び出し
let data = await request(with url)
Task
Swift Concurrencyの処理の基本単位はTaskであり、すべての非同期処理はTask上で実行される。Taskの基本的な使用方法は次の通りで、コンテキストを引き継がずに実行したい場合はTask.detachedとする。また、viewDidLoad()などの同期処理からはasync関数を呼べないので、Task内で実行する必要がある。
// コンテキストを引き継いで非同期処理を実行
// メインスレッドで呼び出した場合、そのままメインスレッドで実行される
Task {
let data = await request(with url)
}
// コンテキストを引き継がずに非同期処理を実行
// メインスレッドで呼び出した場合、それ以外のスレッドで実行される
Task.detached {
let data = await request(with url)
}
また、async letやTaskGroupを使って、Task間でツリー構造(Structured Concurrency)を構築して、非同期処理の待機やキャンセルをこの階層内で行うことができる。
async let
複数の非同期処理を並列実行したい場合、例えば異なるURLからデータを取得する時、それぞれが独立した非同期処理なので、async letを使えば並列で非同期処理を実行し、awaitで両方の結果を待機して処理を行うことができる。
// data1, data2の非同期処理を並列に実行される
async let (data1, _) = URLSession.shared.data(from: url1)
async let (data2, _) = URLSession.shared.data(from: url2)
// 両方の結果を待って実行される
await 非同期処理(data1, data2)
TaskGroup
async letと同様に複数の非同期処理を並列実行したいが、非同期処理の数が動的な場合、TaskGroupを使ってタスクのグループを作成して子タスクを追加できる。
withTaskGroup:タスクグループ内でエラーが発生しない場合に利用
withThrowingTaskGroup:タスクグループ内でエラーが発生する可能性のある場合に利用
エラーが発生した場合、タスクツリーに沿ってエラーが伝搬され、タスクがキャンセルされる
try await withThrowingTaskGroup(of: (Int, Data).self) { group in
for id in ids {
// ループを回して、addTaskで並列実行したい非同期処理の子タスクをグループに追加
group.addTask {
return (id, try await fetchData(withID: id))
}
}
for await (id, data) in group {
// 子タスクの結果を処理する
}
}
Actor
Actorとは、データ競合を防ぐ新しい型で、複数スレッドから同時に同じデータを更新する処理を呼ばれても、Actor内部でデータ競合にならないように防いでくれる。Actorは一度に1つのTaskのみ実行し、それぞれ外の世界から隔離されている(Isolated Actor)。
// class,structのようにactorで宣言するだけでOK
actor Counter {
var count = 0
func increment() -> Int {
value += 1
return value
}
}
// このように複数スレッドから同時にincrementを呼んでも、データ競合は発生せず、順不同で1,2となる
Task.detached {
await counter.increment()
}
Task.detached {
await counter.increment()
}
MainActor
UIの操作はメインスレッドで実行する必要があり、データ競合を防ぎつつメインスレッドで実行できるような特別なActorがMainActorである。 ※MainActorは内部でDispatchQueue.mainを呼び出している。
// 型全体に適用される
@MainActor
class DataSource {
// 暗黙的にMainActorが適用され、メインスレッドで実行される
func update() {}
// MainActorを解除する
// サーバー通信などはメインスレッドで実行すべきではないので、nonisolatedを付ける
nonisolated func fetch() {}
}
class DataSource {
// プロパティやメソッドに個別で適用できる
@MainActor
func update() {}
}
以上、Swift Concurrencyの基本について紹介したが、上記の内容はほんのさわりでしかなく、詳しく知りたい場合は『一冊でマスター!Swift Concurrency入門』がおすすめ。また、記載が間違っている箇所がある場合はご指摘いただけると助かります。