async / await の基本
| 対応: | Swift 5.5(2021) |
|---|
SwiftのSwift Concurrency(Swift 5.5+)では、『async』と『await』を使って非同期処理を同期的なコードのように記述できます。コールバックのネストを解消します。
構文
// async 関数の定義
func 関数名() async -> 型 {
// 非同期処理
}
// エラーを投げる async 関数
func 関数名() async throws -> 型 {
// 非同期処理
}
// await で非同期関数を呼び出す
let result = await 関数名()
let data = try await 非同期関数名()
// async let: 並列実行
async let a = 非同期関数A()
async let b = 非同期関数B()
let (resultA, resultB) = await (a, b)
構文一覧
| 構文 | 概要 |
|---|---|
| async func | 非同期関数を定義します。 |
| await 式 | 非同期関数の完了を待ちます。 |
| async throws func | 非同期かつエラーを投げる関数を定義します。 |
| try await 式 | 非同期かつエラーを投げる関数を呼び出します。 |
| async let | 複数の非同期処理を並列実行します。 |
| Task { } | 非同期コンテキストでコードを実行するタスクを作成します。 |
サンプルコード
async_await_basic.swift
import Foundation
// async throws 関数の定義
func fetchUserName(id: Int) async throws -> String {
// 実際の実装では URLSession.shared.data(from:) などを使う
// ここではシミュレーション
try await Task.sleep(nanoseconds: 100_000_000) // 0.1秒待機
let users = [1: "五条悟", 2: "両面宿儺", 3: "虎杖悠仁"]
guard let name = users[id] else {
throw URLError(.badURL)
}
return name
}
func fetchScore(name: String) async -> Int {
// 『try?』はエラーが発生した場合にnilを返し、エラーを無視する
try? await Task.sleep(nanoseconds: 50_000_000)
return name.count * 10
}
// async 関数は Task から呼び出す
Task {
// 順番に実行
do {
let name = try await fetchUserName(id: 1)
print("名前: \(name)")
let score = await fetchScore(name: name)
print("スコア: \(score)")
} catch {
print("エラー: \(error)")
}
// async let で並列実行
async let name2 = fetchUserName(id: 2)
async let name3 = fetchUserName(id: 3)
do {
let (n2, n3) = try await (name2, name3)
print("並列取得: \(n2), \(n3)")
} catch {
print("エラー: \(error)")
}
}
// 同期コードから Task を使って非同期処理を開始
print("非同期処理を開始しました")
swift async_await_basic.swift 非同期処理を開始しました 名前: 五条悟 スコア: 30 並列取得: 両面宿儺, 虎杖悠仁
エラーハンドリングパターン
非同期処理のエラーハンドリングは同期コードと同じ『do-try-catch』構文で書けます。『async throws』関数の呼び出しには必ず『try await』を付けます。順番は必ず try → await です(await try は誤りです)。
sample_async_error.swift
import Foundation
enum AppError: Error {
case notFound(id: Int)
case networkError(String)
}
// 複数の非同期処理を順序よく実行するパターン
func fetchProfile(userId: Int) async throws -> String {
try await Task.sleep(nanoseconds: 50_000_000)
let profiles = [1: "Gojo Satoru", 2: "Ryomen Sukuna", 3: "Itadori Yuji"]
guard let name = profiles[userId] else {
throw AppError.notFound(id: userId)
}
return name
}
func fetchCursedEnergy(name: String) async throws -> Int {
try await Task.sleep(nanoseconds: 30_000_000)
if name == "Ryomen Sukuna" { return 99999 }
return Int.random(in: 1000...5000)
}
Task {
do {
// 順次実行(前の結果を次に渡します)
let name = try await fetchProfile(userId: 1)
let energy = try await fetchCursedEnergy(name: name)
print("\(name) の呪力: \(energy)")
// 存在しない ID はエラーになります
let _ = try await fetchProfile(userId: 999)
} catch AppError.notFound(let id) {
print("ID \(id) が見つかりません")
} catch {
print("予期せぬエラー: \(error)")
}
}
// Task.sleep でメインスレッドを少し待ちます
Thread.sleep(forTimeInterval: 0.5)
swift sample_async_error.swift Gojo Satoru の呪力: 3241 ID 999 が見つかりません
@MainActor と Actor
Swift Concurrency では『actor』を使ってデータ競合を防ぎます。UIの更新は必ずメインスレッドで行う必要があります。非同期処理の結果を UI に反映する場合は『@MainActor』を使ってメインスレッドに戻ることができます。
| 構文 | 概要 |
|---|---|
| actor | 同時アクセスからデータを保護します。actor のプロパティやメソッドは await で呼び出します |
| @MainActor | クラス・関数・プロパティをメインスレッドで実行することを保証します |
| await MainActor.run { } | 任意の箇所からメインスレッドで処理を実行します |
sample_async_actor.swift
import Foundation
// actor: 複数のタスクからの同時アクセスをシリアル化します
actor ScoreBoard {
private var scores: [String: Int] = [:]
func add(name: String, score: Int) {
scores[name, default: 0] += score
}
func getScore(for name: String) -> Int {
return scores[name, default: 0]
}
}
// @MainActor: UI 更新など、メインスレッドが必要な場合に使います
@MainActor
func updateUI(message: String) {
print("[Main] \(message)")
}
Task {
let board = ScoreBoard()
// 複数の Task から安全に呼び出せます
await withTaskGroup(of: Void.self) { group in
let members = ["Itadori Yuji", "Fushiguro Megumi", "Kugisaki Nobara"]
for member in members {
group.addTask {
await board.add(name: member, score: Int.random(in: 100...500))
}
}
}
let score = await board.getScore(for: "Itadori Yuji")
// UI 更新はメインスレッドで
await updateUI(message: "虎杖のスコア: \(score)")
}
Thread.sleep(forTimeInterval: 0.3)
swift sample_async_actor.swift [Main] 虎杖のスコア: 247
よくあるミス1: 同期コンテキストからのasync呼び出し
非同期関数を同期コンテキストから直接呼ぼうとするとコンパイルエラーになります。async func はそのまま呼べません。
// let result = await fetchProfile(userId: 1) // コンパイルエラー
ミス1: 同期コンテキストで『await』を使うと次のコンパイルエラーが発生します。
error: 'async' call in a function that does not support concurrency
let result = await fetchProfile(userId: 1)
^~~~~~
修正: Task { } で包むと await が使えます。
import Foundation
Task {
// この中では await が使えます
print("Task内のasync処理")
}
よくあるミス2: async letのawait忘れ
async let を宣言したら必ず await で結果を取り出す必要があります。宣言だけして await しないとコンパイラ警告になります。
よくあるミス3: tryとawaitの順序
try と await の順序を間違えるとコンパイルエラーになります。
// let result = await try someAsyncThrows() // コンパイルエラー
ミス3: 『await try』の順序が逆の場合のコンパイルエラーです。必ず『try await』の順で書いてください。
error: 'async' call in a function that does not support concurrency
let result = await try someAsyncThrows()
^~~~~~
// let result = try await someAsyncThrows()
よくあるミス4: Taskのキャンセルチェック漏れ
Task をキャンセルチェックなしで長時間実行すると、キャンセルが効きません。長い処理は Task.isCancelled または try Task.checkCancellation() を呼んでキャンセルを適切に処理します。
sample_async_mistakes.swift
import Foundation
func longTask() async throws {
for i in 1...5 {
try Task.checkCancellation() // キャンセルされていれば CancellationError をthrow
try await Task.sleep(nanoseconds: 10_000_000)
print("進捗: \(i)/5")
}
}
let task = Task {
try? await longTask()
}
// 途中でキャンセル
task.cancel()
Thread.sleep(forTimeInterval: 0.1)
print("タスクがキャンセルされました")
swift sample_async_mistakes.swift Task内のasync処理 進捗: 1/5 タスクがキャンセルされました
概要
Swift Concurrency の async/await を使うことで、コールバックのネスト(コールバック地獄)を解消し、直線的で読みやすい非同期コードを書けます。
『async let』を使うと複数の非同期処理を並列に開始し、全ての結果を await でまとめて待機できます。await は現在のスレッドをブロックせず、処理を一時停止してスレッドを解放します。再開時に同じスレッドに戻る保証はありません(@MainActor を使うと UI スレッドに戻れます)。
Task と TaskGroup についてはTask / TaskGroup / async letを参照してください。
記事の間違いや著作権の侵害等ございましたらお手数ですがこちらまでご連絡頂ければ幸いです。