Swift Concurrency
参考リンク
参考文献
「一冊でマスター!Swift Concurrency入門」佐藤剛士
非同期処理と同期処理の違い
非同期処理とは?
あるタスクで処理を実行中に別のタスクが他の処理を実行することを非同期処理という。あるタスクの処理が完了するのを待つ必要がない。
料理で例えると添付画像のようにAさんが目玉焼きを作っている間に、助っ人のBさんが野菜を切る。
同期処理とは?
プログラムで記述されている順番で処理が実行されることを同期処理という。タスクの処理が完了するのを待つ必要がある。
料理で例えると添付画像のようにAさんがひとりで目玉焼きを作り終えてから野菜を切る。
非同期処理の定義と呼び出し方
async
async
は関数やメソッドを非同期処理として実行することを示す。
func listPhotos(inGallery name: String) async -> [String] {
let result = // ... some asynchronous networking code ...
return result
}
await
非同期なメソッドを呼び出す際、そのメソッドがreturnするまで処理はsuspendされる。そのため非同期処理を行いたい時は、suspendの可能性がある呼び出しの前にawait
を記載する。
let photoNames = await listPhotos(inGallery: "Summer Vacation")
let sortedNames = photoNames.sorted()
let name = sortedNames[0]
let photo = await downloadPhoto(named: name)
show(photo)
await
を使える場所は主に下記の3ヵ所
- async関数・メソッド、プロパティのbody
- @main 属性が付けられた型のmain()メソッドのbody
- Task や Task.detachedのClosure内
Asynchronous Sequences
ループ内でfor await
を用いることで取得できた値から受け取る。
for await i in Counter(howHigh: 10) {
print(i, terminator: " ")
}
// Prints "1 2 3 4 5 6 7 8 9 10"
AsyncSequenceは値を作り出したり含むのではなく、どのようにアクセスするかを定義している。
並列での非同期処理の呼び出し方
async let
下記のコードでは写真をダウンロードする処理が非同期的に呼び出されているが、全てが非同期処理となるので結局1つの写真がダウンロード完了するまで次の写真のダウンロードを開始しない。
しかし、それぞれの写真に依存関係はなく、それぞれ個別にダウンロード可能である。
let firstPhoto = await downloadPhoto(named: photoNames[0]) // 1番目に実行される
let secondPhoto = await downloadPhoto(named: photoNames[1]) // 2番目に実行される
let thirdPhoto = await downloadPhoto(named: photoNames[2]) // 3番目に実行される
let photos = [firstPhoto, secondPhoto, thirdPhoto]
show(photos)
非同期処理を並列で呼び出す場合は定数の前にasync
を付与し、定数を使う際にawait
を記載する。
async let firstPhoto = downloadPhoto(named: photoNames[0])
async let secondPhoto = downloadPhoto(named: photoNames[1])
async let thirdPhoto = downloadPhoto(named: photoNames[2])
let photos = await [firstPhoto, secondPhoto, thirdPhoto]
show(photos)
上記のコードでは3つのdownloadPhoto()の呼び出しが前のダウンロード完了を待たずに開始される。photosはfirstPhoto, secondPhoto, thirdPhotoの結果を用いるため、await
を記載し、「3つの結果が返ってくるまで処理を停止する」ということを示す。
Tasks and Task Groups
前提
Tasks
プログラムの一部として非同期的に実行できる単位の処理
Structured Concurrency
- Task間の親と子の関係によりSwiftがキャンセル処理をある程度行なってくれる
Unstructured Concurrency
- 親Taskがなく、必要に応じて自由に使えるが、コンパイル時にエラーを検知できないのでコードの正しさについては自分で責任を負わないといけない
Task Group
Task GroupにはTaskを追加することが可能
→async - letと比較して優先順位とcancellation をよりコントロールできる
- Task Group内のTaskは同じ親Taskに紐付いており、各Taskは子Taskを持つことができる
- Task Group内の親子関係によりSwiftのコンパイル時にエラーを検知できる
Task Cancellation
Swift ConcurrencyはCooperative Cancellation Modelを使い、各タスクは実行時適切な場所で処理がキャンセルされたかをチェックし、下記の適切な方法で処理のキャンセルに応える。
- CancellationError
- nilを返す
- 部分的に完了した処理を返す
Taskがキャンセルされたかどうかを知る方法
Task.checkCancellation()はタスクがキャンセルされていた場合、CancellationErrorを返す。
またTask.isCancelledを用いて自分でハンドリングすることも可能。
手動でcancelするにはTask.cancel()を呼ぶ。
Actors
同時に走るコード間で安全に情報を共有でき、classのように参照型である。
actor外からアクセスする場合は、プロパティやメソッドの前にawaitを記載する。Actorは可変な状態にアクセスできるtaskを1つしか許可していないため、他のタスクのコードが既にアクセスしていた場合、awaitを用いることで処理を停止する箇所を明示している。
Sendable Types
プログラムの一部で変数やプロパティなど可変な状態を含む箇所をConcurrency Domainと呼ぶ。
重複したアクセスから守ってくれないため、可変なデータ保有するがConcurrency Domain間では共有できないデータの種類も存在する。