はじめに
Swiftで導入されたConcurrencyについて記事にまとめていく。
単純なasync/awaitは元々知っていたが、actorや同時並行で処理できる仕組みなど知らなかったことも結構あったので、この辺りも含めてまとめていく。
Swift Concurrencyの基本概念
iOS開発において、Swift Concurrencyは非同期処理と並行処理を簡潔かつ安全に実装するための強力な機能です。Swift 5.5で導入されたこの機能は、従来のコールバックやクロージャーベースの非同期処理よりも、コードの可読性と安全性を大幅に向上させます。
async/await
非同期処理の核となるのがasync
とawait
キーワードです。
-
async
: 関数が非同期であることを示します。 -
await
: 非同期処理の結果を待つことを示します。
例えば、以下のようにデータを非同期で取得する関数を定義できます:
func fetchData() async throws -> Data {
let url = URL(string: "https://example.com/data")!
let (data, _) = try await URLSession.shared.data(from: url)
return data
}
この方法により、非同期コードを同期コードのように直線的に書くことができ、理解しやすくなります。
Task
Task
を使用すると、非同期処理を開始し管理することができます。
複数の非同期タスクを同時に実行する場合に特に有用です。
Task {
let userData = await fetchUserData()
print(userData)
}
Actor
actor
は、データ競合を防ぐための新しい型です。複数のタスクが同時にデータにアクセスする際の安全性を確保します。
種類としてはActor
、Global Actor
、カスタムGlobal Actor
の3種類ある
Actorの特徴と利点
-
データ競合の防止:Actorは内部状態への同時アクセスを自動的に同期し、データ競合を防ぎます。
-
排他的アクセス:Actorの状態へのアクセスは排他的に行われ、コンパイラーによって強制されます。
-
参照型:Actorは参照型であり、他の型と同様にプロパティ、メソッド、イニシャライザなどを持つことができます。
Global Actorの使用
Global Actorは、アプリケーション全体で共有される特別なActorです。
最も一般的なのは@MainActor
で、UIの更新など、メインスレッドで実行する必要がある処理に使用します。
@MainActor
class GameLibraryViewController: UIViewController {
func updateUI() {
// UIの更新処理
}
}
カスタムGlobal Actorの作成
独自のGlobal Actorを作成することも可能です:
@globalActor
struct MediaActor {
actor ActorType { }
static let shared = ActorType()
}
@MediaActor
var videogames: [Videogame] = []
@MediaActor
func addGame(_ game: Videogame) {
videogames.append(game)
}
注意点
-
Actorのメソッドを外部から呼び出す際は、
await
キーワードを使用する必要があります。 -
Actorは、データの同期を自動的に行うため、明示的なロックは不要です。
-
Global Actorを使用する際は、異なるファイルや型にまたがる宣言でも同じアクター上で処理を実行できます。
Actorsを適切に使用することで、並行処理における多くの問題を回避し、安全で効率的なコードを書くことができます。
MainActorとGlobal Actorの違い
微妙に分かりづらかったので、違いについて以下に記載します。
-
目的と用途:
- MainActor: メインスレッド上で実行されるコンテキストを表し、主にUI関連の操作を安全に行うために使用されます。
- Global Actor: 特定の目的のために作成され、関連するコードやデータを同じアクター上で実行させるために使用されます。
-
定義方法:
- MainActor: システムによって事前に定義されており、
@MainActor
属性を使用して適用します。 - Global Actor: 開発者が独自に定義でき、
@globalActor
属性を使用して作成します。
- MainActor: システムによって事前に定義されており、
-
スコープ:
- MainActor: アプリケーション全体で利用可能な特別なグローバルアクターです。
- Global Actor: 開発者が定義したスコープ内で使用されます。
-
実行コンテキスト:
- MainActor: 常にメインスレッド上で実行されます。
- Global Actor: 開発者が定義したアクター上で実行されます。必ずしもメインスレッドである必要はありません。
-
使用例:
- MainActor: UIの更新やユーザーインターフェースの操作に使用されます。
- Global Actor: 特定のデータや操作を同期させる必要がある場合に使用されます。例えば、アプリケーション全体で共有されるデータの管理などに適しています。
MainActorは特別なGlobal Actorの一種であり、両者はSwiftの並行処理モデルの一部として協調して動作し、スレッドセーフなコードの記述と実行の管理を支援します。
Swift Concurrencyのメリット
- コードの可読性向上:非同期処理を直線的に書けるため、理解しやすくなります。
- エラーハンドリングの簡素化:従来のコールバックよりも簡単にエラーを処理できます。
- データ競合の防止:
actor
を使用することで、並行処理時のデータ競合を防ぐことができます。
実践的な使用例
func downloadImage(from url: URL) async throws -> UIImage {
let (data, _) = try await URLSession.shared.data(from: url)
guard let image = UIImage(data: data) else {
throw NSError(domain: "ImageError", code: 0, userInfo: nil)
}
return image
}
// 使用例
Task {
do {
let image = try await downloadImage(from: URL(string: "https://example.com/image.jpg")!)
// UIImageViewなどに表示する処理
} catch {
print("画像のダウンロードに失敗しました: \(error)")
}
}
この例では、画像を非同期でダウンロードし、エラーハンドリングも行っています。async
/await
を使用することで、コードの流れが分かりやすくなっています。
withCheckedContinuation
withCheckedContinuation
は、Swift Concurrencyにおいて、従来のコールバックベースの非同期APIを新しいasync/await
モデルに適応させるための機能です。
主な特徴と使用方法は以下の通りです:
-
目的:コールバックを使用する古い非同期コードを、新しい
async/await
スタイルのコードに変換します。 -
基本的な使い方:
func asyncFunction() async -> Result { await withCheckedContinuation { continuation in oldCallbackFunction { result in continuation.resume(returning: result) } } }
-
安全性:
withCheckedContinuation
は使用方法のチェックを行います。例えば、resume()
を複数回呼び出すとエラーが発生し、一度も呼び出さない場合はログを出力します。 -
エラーハンドリング:エラーをスローする必要がある場合は、
withCheckedThrowingContinuation
を使用します。 -
使用例:
- シンプルなコールバック:
withCheckedContinuation
を使用 - エラーを含むコールバック:
withCheckedThrowingContinuation
を使用 - Result型のコールバック:
continuation.resume(with: result)
を使用
- シンプルなコールバック:
-
注意点:
withCheckedContinuation
の使用は、完全に新しいasync/await
スタイルのコードに移行するまでの橋渡しとして考えるべきです。
withThrowingTaskGroup
withThrowingTaskGroup を使用して同時並行で処理を実行することも可能。
しかし、例えば複数APIを同じ画面でリクエストしたい場合、開発者的には意図した順番で処理する方が安全だと思われるので、同時並行しても問題無い場合のみかどうか検討する必要がある。
注意点
- iOS 13以降で使用可能:Swift Concurrencyは、iOS 13以降のデバイスで使用できます。
- Xcode 13.3.1以降を推奨:特にiOS 13、14での使用には、Xcode 13.3.1以降が推奨されます。
- Swift 6での変更:将来的にSwift 6では、
Sendable
プロトコルの使用が必須になる可能性があります。これは並行処理時のデータ安全性をさらに高めるためです。
参考文献