【iOS】Swift Concurrencyに入門してみた
IT技術
はじめに
Swift Concurrencyを勉強したので、備忘録を残しておこうと思います
Swift Concurrency
Swift Concurrencyとは、Swift5.5から導入された言語機能で非同期処理と並列処理の実装をサポートします
async/await
関数のreturn矢印の前にasyncをつけることで非同期関数として扱えるようになり、
awaitをつけて呼び出すことで処理が終わるまで実行が一時停止します
以下はコールバック処理で書いた場合とそれをasync/awaitに書き換えた場合の例です
1// コールバックで書いたもの
2func hello(completion: (String) -> ()) {
3 sleep(3) // 時間のかかる処理
4 completion("Hello")
5}
6
7func world(completion: (String) -> ()) {
8 sleep(3) // 時間のかかる処理
9 completion("World")
10}
11
12// 呼び出し
13hello { hello in
14 world { world in
15 print(hello + world)
16 }
17}
1// async/awaitで書いたもの
2func hello() async -> String {
3 try? await Task.sleep(until: .now + .seconds(3)) // 時間のかかる処理
4 return "Hello"
5}
6
7func world() async -> String {
8 try? await Task.sleep(until: .now + .seconds(3)) // 時間のかかる処理
9 return "World"
10}
11
12// 呼び出し
13Task {
14 let hello = await hello()
15 let world = await world()
16 print(hello + world)
17}
比較してみると、コールバックではネストする必要がありますが、async/awaitでは同期的にコードを書けていることが分かります
エラーが発生する非同期関数では、throwsの前にasyncをつけることで非同期関数にします
呼び出し時には、通常と同じようにtryをつけて呼び出します
1// エラーが発生する場合
2func hello() async throws -> String {
3 try await Task.sleep(until: .now + .seconds(3)) // 時間のかかる処理
4 return "Hello"
5}
6
7func world() async throws -> String {
8 try await Task.sleep(until: .now + .seconds(3)) // 時間のかかる処理
9 return "World"
10}
11
12// 呼び出し
13Task {
14 do {
15 let hello = try await hello()
16 let world = try await world()
17 print(hello + world)
18 } catch {
19 print("error")
20 }
21}
async let
async letを使うことで非同期関数を並列に実行することができます
1func hello() async -> String {
2 try? await Task.sleep(until: .now + .seconds(3)) // 時間のかかる処理
3 return "Hello"
4}
5
6func world() async -> String {
7 try? await Task.sleep(until: .now + .seconds(3)) // 時間のかかる処理
8 return "World"
9}
10
11// 呼び出し
12Task {
13 async let hello = hello()
14 async let world = world()
15 await print(hello + world)
16}
コールバック関数を非同期関数へ変換
withCheckedContinuationを使うことで既存のコールバック関数を非同期関数へ変換することができます
エラーが発生するコールバック関数の場合は、withCheckedThrowingContinuationを使用します
1// コールバック関数を非同期関数に変換
2// コールバック関数
3func hello(completion: (String) -> ()) {
4 sleep(3) // 時間のかかる処理
5 completion("Hello")
6}
7
8// 非同期関数に変換
9func asyncHello() async -> String {
10 return await withCheckedContinuation { continuation in
11 hello { hello in
12 continuation.resume(returning: hello)
13 }
14 }
15}
16
17// 呼び出し
18Task {
19 let hello = await asyncHello()
20 print(hello)
21}
1// エラーが発生するコールバック関数を非同期関数に変換
2struct MyError: Error {}
3// エラーが発生するコールバック関数
4func hello(completion: (Result<String, Error>) -> ()) {
5 sleep(3) // 時間のかかる処理
6 let hasError = Bool.random()
7 if hasError {
8 completion(.failure(MyError()))
9 } else {
10 completion(.success("Hello"))
11 }
12}
13
14// 非同期関数に変換
15func asyncHello() async throws -> String {
16 return try await withCheckedThrowingContinuation { continuation in
17 hello { result in
18 continuation.resume(with: result)
19 }
20 }
21}
22
23// 呼び出し
24Task {
25 do {
26 let hello = try await asyncHello()
27 print(hello)
28 } catch {
29 print("Error")
30 }
31}
Task
タスクは非同期処理の実行単位です
Taskを使うことで同期関数から非同期関数を呼び出すことができるようになります
Task.initは現在の実行環境を引き継ぎます
@MainActorでTask.initを使った場合、タスクは@MainActorで実行されます
1// 呼び出し
2Task {
3 let hello = await hello()
4 print(hello)
5}
6
7// @MainActorとすることで明示的にMainActorで実行
8Task { @MainActor in
9 let hello = await hello()
10 print(hello)
11}
Task.detachedは現在の実行環境を引き継ぎません
メインスレッドで実行する必要がないものはTask.detachedを使用します
1// 呼び出し
2Task.detached {
3 let hello = await hello()
4 print(hello)
5}
Taskのインスタンスを保持することでタスクをキャンセルすることができます
以下のコードでは、タスクをキャンセルすることでworld()が実行されずにエラーが発生して終了します
1func hello() async throws -> String {
2 try Task.checkCancellation()
3 sleep(3)
4 return "Hello"
5}
6
7func world() async throws -> String {
8 try Task.checkCancellation()
9 sleep(3)
10 return "World"
11}
12
13// 呼び出し
14let task = Task {
15 do {
16 let hello = try await hello()
17 let world = try await world()
18 print(hello + world)
19 } catch {
20 print("error")
21 }
22}
23
24// タスクをキャンセル
25task.cancel()
TaskGroup
TaskGroupを使うことで並列処理を実装することができます
TaskGroupの作成にはwithTaskGroupを使用します
エラーが発生する場合はwithThrowingTaskGroupを使用します
以下のコードではhello()とworld()が並列に実行され、"HelloWorld"または"WorldHello"で出力されます
1func hello() async -> String {
2 try? await Task.sleep(until: .now + .seconds(3)) // 時間のかかる処理
3 return "Hello"
4}
5
6func world() async -> String {
7 try? await Task.sleep(until: .now + .seconds(3)) // 時間のかかる処理
8 return "World"
9}
10
11Task {
12 var printText = ""
13 await withTaskGroup(of: String.self) { group in
14 // タスクを作成
15 group.addTask {
16 return await hello()
17 }
18
19 // タスクを作成
20 group.addTask {
21 return await world()
22 }
23
24 for await text in group {
25 printText += text
26 }
27 }
28 print(printText)
29}
タスクのキャンセル
CancellationErrorをスローすることでタスクをキャンセルできます
タスクがキャンセルされている場合、Task.checkCancellationではCancellationErrorがスローされます
また、Task.isCancelledでタスクがキャンセルされているか判定することもできます
1func hello() async throws -> String {
2 // タスクがキャンセルされている場合、エラーをスローして処理を抜ける
3 try Task.checkCancellation()
4 sleep(3)
5 return "Hello"
6}
7
8func world() async throws -> String {
9 // タスクがキャンセルがされているか判定
10 if Task.isCancelled {
11 // エラーをスローして処理を抜ける
12 throw CancellationError()
13 }
14 sleep(3)
15 return "World"
16}
17
18// 呼び出し
19let task = Task {
20 do {
21 let hello = try await hello()
22 let world = try await world()
23 print(hello + world)
24 } catch {
25 print("error")
26 }
27}
28
29// タスクをキャンセル
30task.cancel()
actor
actorはデータ競合を防ぐ型でclassと同じ参照型です
プロパティ、メソッド、イニシャライザの定義とプロトコル準拠ができますが、actorの型を継承することはできません
プロパティやメソッドを使う時にawaitをつけることでデータ競合を防ぐ仕組みがあります
actorのプロパティは外から直接更新することはできません
nonisolatedをつけることでactor管理から外れ、awaitなしでアクセスできるようになります
1// actorを使っていない場合
2var count = 0
3
4func increment() async -> Void {
5 count += 1
6}
7
8// 呼び出し
9// 複数同時に呼び出しすると、1から順番に出力されない場合がある
10Task.detached {
11 await increment()
12 print(count)
13}
1// actorを使う場合
2actor Counter {
3 var count = 0
4
5 func increment() {
6 count += 1
7 }
8}
9
10let counter = Counter()
11
12// 呼び出し
13// 複数同時に呼び出しても、1から順番に出力される
14Task {
15 await counter.increment()
16 print(await counter.count)
17}
@MainActor
@MainActorに適応したものはメインスレッドで実行されます
nonisolatedをつけることでMainActor管理から外れ、awaitなしでアクセスできるようになります
型、コンピューテッドプロパティ、メソッドに@MainActor属性を付けることができますが、ストアドプロパティーに@MainActorを適応することはできません
1// 型に@MainActorを適応
2@MainActor
3struct Hoge {}
4
5struct Fuga {
6 private var _hoge = "hoge"
7
8 // コンピューテッドプロパティに@MainActorを適応
9 @MainActor
10 var hoge: String {
11 get { _hoge }
12 set { _hoge = newValue }
13 }
14
15 // メソッドに@MainActorを適応
16 @MainActor
17 func fuga() {}
18}
Async Sequence
Async Sequenceは要素への非同期、順次、反復アクセスを提供する型で、filter、contains、mapなどのメソッドが標準で用意されています
AsyncSequenceとAsyncIteratorProtocolに準拠することで独自の型を作成することができます
1struct AsyncTimer: AsyncSequence {
2 typealias Element = Int
3
4 struct AsyncIterator: AsyncIteratorProtocol {
5 private var count = 0
6 private var date = Date()
7
8 mutating func next() async -> Element? {
9 guard count < 10 else {
10 // nilを返すとループが終了
11 return nil
12 }
13
14 while date.distance(to: Date()) < 1 {
15 try? await Task.sleep(nanoseconds: 1 * 10_000_000)
16 }
17
18 date.addTimeInterval(1)
19 count += 1
20 return count
21 }
22 }
23
24 func makeAsyncIterator() -> AsyncIterator {
25 return AsyncIterator()
26 }
27}
28
29// 呼び出し
30Task {
31 let double = AsyncTimer().map { $0 * 2 }
32
33 for await time in double {
34 print(time)
35 }
36}
Sendable
Sendableはデータ競合を防ぐための型です
actorは参照型ですが、同時アクセスを防ぐためSendableといえます
外からactor内のメソッドを呼び出す場合には、引数はSendableな値である必要があります
@Sendableをメソッド、クロージャにつけることで、Sendableとして扱うことができます
structとenumはSendableな値(プロパティ、メソッドなど)のみを持つ場合、暗黙的にSendableに準拠されます(publicな型の場合は例外)
1struct Hoge: Sendable {}
2
3@Sendable
4func hoge() {}
5
6let fuga: (@Sendable () -> Void) = {}
おわりに
今回、Swift Concurrencyの書き方をまとめてみました。
Swift Concurrencyを使用する時には、この記事を見返しながら実装をしたいと思います。
この記事が何かお役に立てば幸いです。
ここまでご覧いただきありがとうございました。
ライトコードでは、エンジニアを積極採用中!
ライトコードでは、エンジニアを積極採用しています!社長と一杯しながらお話しする機会もご用意しております。そのほかカジュアル面談等もございますので、くわしくは採用情報をご確認ください。
採用情報へ
業務ではiOS開発に携わらせていただいています。 まだまだ分からないことだらけで、日々分からないことと戦いながら仕事をしている者です。 ブログ記事は暖かい目で見ていただけるとありがたいです。