0.はじめに
モバイルアプリエンジニアに興味をもち、Swiftの学習を進めている大学院生です!
Qiita初投稿です。間違っている点や質問がございましたらビシバシお願いします!!
なぜSwift Concurrencyを学ぼうと思ったか
TCAのチュートリアル中に次のような警告が、、、
import ComposableArchitecture
import Foundation
import Testing
@testable import TCATutorial
@MainActor
struct NumberFactClient {
var fetch: @Sendable (Int) async throws -> String
}
extension NumberFactClient: DependencyKey {
static let liveValue = Self(
fetch: { number in
let(data, _) = try await URLSession.shared
.data(from: URL(string: "http://numbersapi.com/\(number)")!)
return String(decoding: data, as: UTF8.self)
}
)
}
Main actor-isolated static property 'liveValue' cannot be used to satisfy nonisolated requirement from protocol 'DependencyKey'; this is an error in the Swift 6 language mode
調べたところ、swift6からConcurrencyの扱いが厳しくなっているようです。
https://developer.apple.com/documentation/swift/adoptingswift6
Swift Concurrencyに関しての記事は散々出回っていますが、自分の理解を深めるためにも投稿させていただきます。
https://docs.swift.org/swift-book/documentation/the-swift-programming-language/concurrency/#Actors
目次
1. async/await
Swift Concurrencyで1番最初に出会うのが基本的な async
await
です。
まずはサンプルコード。
import Foundation
func cookPasta() async -> String {
try? await Task.sleep(for: .seconds(2))
print("🍝 パスタができた")
return "🍝"
}
func cookCurry() async -> String {
try? await Task.sleep(for: .seconds(5))
print("🍛 カレーができた")
return "🍛"
}
func makeSalad() -> String {
print("🥗 サラダを作った")
return "🥗"
}
func serve(_ curry: String, _ pasta: String, _ salad: String) {
print("配膳: \(curry), \(pasta), \(salad)")
}
@main
struct Main {
static func main() async {
print("🍽 調理スタート")
let curryResult = await cookCurry() // カレーを煮込む(5秒)
let pastaResult = await cookPasta() // パスタを茹でる(2秒)
let saladResult = makeSalad() // サラダはすぐ作れる
print("🧾 料理の準備完了")
serve(curryResult, pastaResult, saladResult)
}
}
🍽 調理スタート
🍛 カレーができた
🍝 パスタができた
🥗 サラダを作った
🧾 料理の準備完了
配膳: 🍛, 🍝, 🥗
料理人の動きにたとえるなら…
- カレーを煮込む → 完成を待ってから…
- パスタを茹でる → それが終わってから…
- サラダを作る(これはすぐ終わる)
というように、キッチンでは一人のコックさんが順番に料理している状態です。
async/awaitを使っていても、順番に await
すればこのような直列的な動きになります。
2. async let
じゃあ、もし「カレーとパスタを同時に調理」できたらもっと早くなるでしょうか?
async let
を使うことで、非同期関数を並行に実行することができます。
struct Main {
static func main() async {
print("🍽 調理スタート")
async let curry = cookCurry()
async let pasta = cookPasta()
let salad = makeSalad()
let curryResult = await curry
let pastaResult = await pasta
print("🧾 料理の準備完了")
serve(curryResult, pastaResult, salad)
}
}
注目してほしいのは、curryResult
を先にawait
しているにもかかわらず、先に 🍝 パスタができた
が出力されている点です。
これは、async let
によって カレーとパスタが同時に調理されているため、それぞれが独立して進行し、早く終わったパスタの出力が先に現れたということです。
🍽 調理スタート
🥗 サラダを作った
🍝 パスタができた
🍛 カレーができた
🧾 料理の準備完了
配膳: 🍛, 🍝, 🥗
3. TaskとTask Group
Taskの公式ドキュメント
2章の、async let
では、暗黙的に子タスクを生成しています。
Task Groupでは、並列実行する数を動的に決めることができます。
import Foundation
func cook(_ dish: String) async -> String {
let seconds = [
"カレー": 5,
"パスタ": 2,
"サラダ": 1,
"スープ": 3,
"ケーキ": 4,
"ジュース": 1,
"ピザ": 10
][dish, default: 2]
try? await Task.sleep(for: .seconds(UInt64(seconds)))
print("\(dish) ができた")
return dish
}
料理を増やしました。調理時間の異なるメニューを複数用意し、メニューに記載されている料理一つ一つに、withTaskGroup(of: String.self){ group in ...}
で、子タスクを集めるグループを作って、クロージャの中で処理してください。というような記述をしています。そして、menu
に記載された料理をgroup.addTask{ await cook(dish) }
でタスクとして追加しています。
@main
struct Main {
static func main() async {
print("🍽 TaskGroupによる調理スタート")
let menu = ["カレー", "ピザ", "サラダ", "スープ", "ケーキ"]
let results = await withTaskGroup(of: String.self) { group in
for dish in menu {
group.addTask {
await cook(dish)
}
}
var completed: [String] = []
for await result in group {
completed.append(result)
}
return completed
}
print("🧾 全料理完成: \(results.joined(separator: ", "))")
}
}
completed
に完成した順で料理を.append
して、最終的に完成したら全料理完成で表示します。
実行結果を見ると、並行にcook(dish)
が実行されたことによって、早くできたものから順にcompleted
に.append
されているのがわかります。
このように、async let
のようにあらかじめ実行するタスクが決まっていない時にTask Group
は効果を発揮します。
🍽 TaskGroupによる調理スタート
サラダ ができた
スープ ができた
ケーキ ができた
カレー ができた
ピザ ができた
🧾 全料理完成: サラダ, スープ, ケーキ, カレー, ピザ
4. Task Cancellation
「通信が遅い!」「読み込むデータが重すぎる!」という時にすべてのタスクが完了するのを待たずにユーザーがこの作業を停止できるようにするには、タスクでキャンセルを確認し、キャンセルされた場合は実行を停止する必要があります。Swiftではcooperative cancellation modelを採用しています。cooperative cancellation modelのイメージは以下の通りです。
タスクがキャンセルされたからといって自動で気に止まるのではなく、キャンセルされたかどうかを自分で確認して、適切に止める。
そして、キャンセルの方法は二つあります。
checkCancellation
を呼び出すと、タスクがキャンセルされた場合にエラーがスローされます。エラーが伝播することで、タスクの全ての作業を停止できます。
キャンセルその1.Task.checkCancellation()
この処理では、CancellationError
がthrowされたタイミングでタスクをキャンセルします。throw関数で、中断したい処理を明確に示すときは、こちらを使うのが最適です。
import Foundation
func cook(_ dish: String) async throws -> String {
let seconds = [
"カレー": 5,
"パスタ": 2,
"サラダ": 1,
"スープ": 3,
"ケーキ": 4,
"ジュース": 1,
"ピザ": 10
][dish, default: 2]
try Task.checkCancellation()
print("\(dish)の調理を開始")
try await Task.sleep(for: .seconds(UInt64(seconds)))
print("\(dish) ができた")
return dish
}
@main
struct Main {
static func main() async {
print("🍽 TaskGroupによる調理スタート")
let menu = ["カレー", "ピザ", "サラダ", "スープ", "ケーキ"]
let results = await withTaskGroup(of: String?.self) { group in
for dish in menu {
if dish == "スープ" {
group.cancelAll()
print("スープの調理前にキャンセルが実行されました。")
}
group.addTask {
do{
return try await cook(dish)
} catch is CancellationError {
print("\(dish)の調理はキャンセルされました。")
return "\(dish)(キャンセル)"
} catch {
print("\(dish)で予期せぬエラー : \(error)")
return "\(dish) エラー"
}
}
}
var completed: [String] = []
for await result in group {
if let result {
completed.append(result)
}
}
return completed
}
print("🧾 完成した料理: \(results.joined(separator: ", "))")
}
}
実行結果を見ると、キャンセル通知が来たタイミングで、即時実行中のタスクが中断され、出力されていることがわかります。
🍽 TaskGroupによる調理スタート
カレーの調理を開始
ピザの調理を開始
サラダの調理を開始
スープの調理前にキャンセルが実行されました。
カレーの調理はキャンセルされました。
ピザの調理はキャンセルされました。
スープの調理はキャンセルされました。
サラダの調理はキャンセルされました。
ケーキの調理はキャンセルされました。
🧾 完成した料理: スープ(キャンセル), ピザ(キャンセル), カレー(キャンセル), サラダ(キャンセル), ケーキ(キャンセル)
キャンセルその2.Task.isCancelled
isCancelledはtrue
かfalse
を返すのみの処理です。サンプルコードでは、isCancelledをguard文でチェックして、true
になったタイミングで、groupの子タスクをキャンセルします。つまり、キャンセルされる前の実行済みタスクの値は処理され、出力されるわけです。
let menu = ["カレー", "ピザ", "サラダ", "スープ", "ケーキ"] //注文リスト
let results = await withTaskGroup(of: String?.self) { group in
for dish in menu {
if dish == "ケーキ" {
print("ケーキの調理前にキャンセルが実行されました。")
group.cancelAll()
}
let added = group.addTaskUnlessCancelled {
guard !Task.isCancelled else {
print("\(dish)はキャンセルされました。")
return nil
}
return await cook(dish)
}
guard added else {
print("\(dish)の追加に失敗しました(キャンセル)")
break
}
}
実行結果を見ると、スープがキャンセルされる前に処理が完了したカレー
は完成して、スープがキャンセルされたタイミングでまだ処理中だったメニューが全部キャンセルされています。isCancelled
を随時確認することで、キャンセルかどうかをみて、"キャンセルされました"
と表示するかどうかを判断しています。
🍽 TaskGroupによる調理スタート
スープの調理前にキャンセルが実行されました。
スープの追加に失敗しました(キャンセル)
ケーキの追加に失敗しました(キャンセル)
ピザはキャンセルされました。
サラダはキャンセルされました。
カレー ができた
🧾 完成した料理: カレー
5. Unstructed Concurrency
Swift Concurrencyは、Structured Concurrencyが基本です。これは、1、2、3章のasync let
や TaskGroup
のように「親タスク ⇄ 子タスク」の関係を明示的に持ちます。
それに対して、Unstructured Concurrencyは:
-
親タスクに属さないタスク
-
自分自身で完全に制御する必要がある
つまり、より柔軟に、でもより責任を持ってタスクを管理するためのスタイルです。
「ある処理だけ完全にバックグラウンドで動かしたい」「親と切り離した一時的な処理を動かしたい」「Viewが消えても続けたい処理がある」といったときに使われるのがUnstructedConcurrencyです。
import Foundation
func cook(_ dish: String) async {
print("🧑🍳 \(dish)の調理を開始")
try? await Task.sleep(for: .seconds(2))
print("\(dish)が完成しました")
}
@main
struct Main {
static func main() async {
print("🍽 調理スタート")
let menu = ["カレー", "ピザ", "サラダ", "スープ", "ケーキ"]
Task {
await cook(menu[0]+"(Task)")
}
Task.detached {
await cook(menu[1]+"(Detached)")
}
try? await Task.sleep(for: .seconds(3))
print("調理完了")
}
}
🍽 調理スタート
🧑🍳 ピザ(Detached)の調理を開始
🧑🍳 カレー(Task)の調理を開始
カレー(Task)が完成しました
ピザ(Detached)が完成しました
調理完了
6. Actor
Swift Concurrencyでは データ競合(Data Race) を避けるためにActor
という仕組みが用意されています。
料理人の世界で言うと「冷蔵庫」のようなもの
🍽 料理人たちはそれぞれ同時に調理していて、冷蔵庫の中身(在庫)を同時に触ろうとしたら、どちらかが冷蔵庫を離れるまで待ちます!っていうイメージ。
以下のactor
で定義しているFridge
には以下の在庫(データ)があります。
- トマト:3個
- たまねぎ:2個
take(item, chef)
で、料理人(シェフ)が特定の食材を取り出す処理を担当します。
この関数を複数のシェフが同時に呼び出しても、actorはアクセスを順番にさばくので、
在庫データ(stock)を安全に扱うことができます。
actor Fridge {
private var stock: [String: Int] = ["トマト": 3, "たまねぎ": 2]
func take(_ item: String, by chef: String) -> Bool {
print("👨🍳 \(chef) が「\(item)」を取りに来た")
if let count = stock[item], count > 0 {
stock[item]! -= 1
print("✅ \(chef) は「\(item)」を取り出した(残り: \(stock[item]!))")
return true
} else {
print("❌ \(chef) は「\(item)」を取れなかった(在庫なし)")
return false
}
}
func checkStock() -> [String: Int] {
return stock
}
}
fridge
はactor
として宣言されているので、async
でシェフたちが同時に冷蔵庫にアクセスしようとしても、順番に冷蔵庫へのアクセスを許可するので、データ競合が起きません。
@main
struct Main {
static func main() async {
let fridge = Fridge()
async let chef1 = fridge.take("トマト", by: "シェフ1")
async let chef2 = fridge.take("トマト", by: "シェフ2")
async let chef3 = fridge.take("トマト", by: "シェフ3")
let results = await [chef1, chef2, chef3]
print("取り出し結果: \(results)")
let stock = await fridge.checkStock()
print("最終在庫: \(stock)")
}
}
実行すると、それぞれの料理人が別のスレッドから冷蔵庫のアイテムを順番に取っているのがわかります。
Actorを使うことで、データ競合を防ぎながら並行処理を行えます。
👨🍳 シェフ1 が「トマト」を取りに来た
✅ シェフ1 は「トマト」を取り出した(残り: 2)
👨🍳 シェフ3 が「トマト」を取りに来た
✅ シェフ3 は「トマト」を取り出した(残り: 1)
👨🍳 シェフ2 が「トマト」を取りに来た
✅ シェフ2 は「トマト」を取り出した(残り: 0)
取り出し結果: [true, true, true]
最終在庫: ["トマト": 0, "たまねぎ": 2]
7. isolatedとnonisolated
概念と使い方
actor
は、データ競合を防ぐために1つのタスクずつしかアクセスされない仕組みでした。
ただし、そのままだと、全てが順番待ちになってしまうので、以下のような問題が出てきます。
- イミュータブルな値を確認したいだけなのに順番待ち
- 関数を一部だけ切り出して再利用できない
そこで登場するのがisolated
、nonisolated
です。
Fridge
に新たにその冷蔵庫の識別番号や製造日といった、不変なactor
内が持つ変数を追加してみました。
actor Fridge {
let ID: String
let modelName: String
let capacity: Int
let createdAt: Date
init(ID: String, modelName: String, capacity: Int, createdAt: Date) {
self.ID = ID
self.modelName = modelName
self.capacity = capacity
self.createdAt = createdAt
}
// ...省略...
// 状態に触れないラベル → nonisolatedでawait不要
nonisolated func printLabel() {
print("🧾 製造ラベル: [ ID: \(ID), 機種名: \(modelName), 容量: \(capacity)L, 製造日: \(createdAt.formatted(date: .abbreviated, time: .omitted)) ]")
}
}
ついでに、isolate
の例として、add()
関数を用意しました。
func add(_ item: String, _ count: Int, by supplier: String) {
print("🚚 \(supplier) が食材を補充した(\(item): \(count)個)")
stock[item, default: 0] += count
}
レストランの営業中に電気業者さんが冷蔵庫の定期点検に来ました。
複数人のシェフが順番に食材を冷蔵庫に直しているところに電気業者さんを並ばせて待つわけにはいきませんね。
そもそも冷蔵庫の製造日やIDって、冷蔵庫が作られた時から不変なものなので、データが競合する心配すらありません。イミュータブルで宣言されたプロパティは初期化後に変更されないため、非同期アクセスでもデータ競合が発生しないことが保証されています。
製造業者は在庫状態へはアクセスせずに、冷蔵庫の概要だをみます。こういったものをnonisolated
で管理することで、await
で待たずにアクセスすることを可能にします。
@main
struct Main {
static func main() async {
let fridge = Fridge(
ID: "SCO1100",
modelName: "業務用冷蔵庫",
capacity: 300,
createdAt: Date()
)
async let chef1 = fridge.take("トマト", by: "シェフ1")
async let chef2 = fridge.take("トマト", by: "シェフ2")
async let chef3 = fridge.take("たまねぎ", by: "シェフ3")
let results = await [chef1, chef2, chef3]
print(results)
print("\n🔧 製造業者が点検にやってきた。「ラベル見せてくれる?」")
fridge.printLabel() // ← nonisolated なので await 不要
await fridge.add("トマト", 20, by: "シェフ4")
print("\n📊 最終在庫状況↓")
let stock = await fridge.currentStock()
print("\(stock)")
}
}
シェフはデータ競合することなく安全に冷蔵庫から食材を取り出したり、追加したりを行い、業者は待たずとも冷蔵庫の情報を取得できます。
👨🍳 シェフ1 が「トマト」を取りに来た
✅ シェフ1 は「トマト」を取り出した(残り: 2)
👨🍳 シェフ2 が「トマト」を取りに来た
✅ シェフ2 は「トマト」を取り出した(残り: 1)
👨🍳 シェフ3 が「たまねぎ」を取りに来た
✅ シェフ3 は「たまねぎ」を取り出した(残り: 1)
[true, true, true]
🔧 製造業者が点検にやってきた。「ラベル見せてくれる?」
🧾 製造ラベル: [ ID: SCO1100, 機種名: 業務用冷蔵庫, 容量: 300L, 製造日: 2025年4月12日 ]
🚚 シェフ4 が食材を補充した(トマト: 20個)
📊 最終在庫状況↓
["トマト": 21, "たまねぎ": 1]
外部関数としてactor内の関数を使う
actor
内で定義した関数をactor
の外で再利用したいとき、普通に書くとisolatedな状態に触れられずにエラーになります。
トマトを仕入れる関数を、add()
関数を再利用する形で実装していきます。
func restockTomato(_ count: Int, to fridge: Fridge, by chef: String) {
fridge.add("トマト", count, by: supplier)
}
@main
struct Main {
static func main() async {
let fridge = Fridge()
restockTomato( 10, to: fridge, by: "トマト運送") // ← ここでエラーの原因となる関数を呼び出し
}
}
しかし以下のようなエラーが出ます。
Call to actor-isolated instance method 'add(_:_:by:)' in a synchronous nonisolated context
isolated
じゃない引数からactor
にはアクセスできません。そこで、fridge
にisolated
をつけてあげることで、actor
内のisolated
な関数を再利用することができます。
ここで注意するのは、actor
の関数を再利用している、つまり、一度に状態へアクセスできる処理は1つであるということです。利用している部分にはawait
をつける必要があります。
func restockTomato(_ count: Int, to fridge: isolated Fridge, by supplier: String) {
fridge.add("トマト", count, by: supplier)
}
@main
struct Main {
static func main() async {
let fridge = Fridge()
await restockTomato( 10, to: fridge, by: "トマト運送") //actorのisolatedな関数なのでawaitが必要!!
}
}
🚚 トマト運送 が食材を補充した(トマト: 10個)
8. Sendable Types
https://techblog.zozo.com/entry/swift6-strict-concurrency-checking
まず、SendableTypesについて理解する前に、Concurrency Domainというワードについて説明します。
これは、「Task
やactor
で並行処理実行の安全が保証されたエリアのくくり」
本記事では、
-
Task
→ 調理台での調理 -
actor
→ 冷蔵庫
としてまとめてきました。この調理台での調理の処理、冷蔵庫の在庫追加、取り出しの処理それぞれをConcurrency Domain
といいます。これらは異なるDomain内での処理が行われていますが、以下のように異なるDomainへのデータをやり取りするシチュエーションがあり得ます。
struct Ingredient {
var name: String
var amount: Int
}
actor Fridge{
//...省略...
func store(_ ingredient: Ingredient) {
print("冷蔵庫: \(ingredient.name) を \(ingredient.amount)個補充します")
stock[ingredient.name, default: 0] += ingredient.amount
}
}
@main
struct Main {
Task.detached {
let tomato = Ingredient(name: "トマト", amount: 5)
await fridge.store(tomato) // ← Task → Actor にデータを渡してる!
}
}
このようなとき、もしtomato
が変更可能であったり、複数人の人が同時に触るデータであったとき、
「誰かがトマトの数を減らしている間に、別の誰かが数を参照した」
という状況が起こり得ます。これをデータ競合といいます。
これは実際の開発では
- アプリがクラッシュする
- データの整合性が崩れる
- 予測不能な動作になる
といったバグの温床になりかねません。
Swiftでは、Concurrency Domainを跨いだやり取りしてもデータ競合が起きない保証があるものをSendableを準拠させることで作れます。
Sendableプロトコルがに準拠させることができる条件
- 値型(structやenumなど)であり、変更可能な状態がSendableなデータで構成されていること。
- 例: 全てのプロパティがSendableな構造体や、Sendableな関係値を持つ列挙型など。
- 変更可能な状態を一切持っていない。不変な状態が他のSendableなデータで構成されていること。
- 例:
let
定義のみの構造体。
- 例:
- 変更可能な状態の安全性が、コードによって保証されている。
- 例:
@MainActor
がつけられたクラスや、特定のスレッドやキュー状でプロパティへのアクセスを直列化しているクラスなど。
これらに該当する型は、Swiftで安全にConcurrency Domain間でやり取りできると認識され、Sendable
として扱うことができます。
- 例:
10 まとめ
async/await
async let
TaskとTask Group
nonisolated
Sendable