SwiftConcurrencyについて調べたので備忘録

* 最近SwiftConcurrencyに既存プロジェクトを置き換えたりする中で色々と調べたので備忘録として書き残します。
    * 主に`async/await`, `Async(Throwing)Stream`, `Task`についての記事になります。

# async/awaitの基本的な使い方

## asyncとawaitについて
* `async`はメソッドが非同期作業を実行することを明確にするメソッド属性です。

  * 例としてAppleが提供しているTask型のstaticメソッドsleepなどがあります。
```swift
public static func sleep(nanoseconds duration: UInt64) async throws
```

* `await`は`async`な非同期メソッドを呼び出す際に使われるキーワードです。
　
```swift
try await Task.sleep(nanoseconds: UInt64(1 * 1_000_000_000))
```

* このようにasyncとawaitは対をなす関係にあります。

## 直列実行パターン
`e.g. API1からUser一覧を取得してAPI2からUserの詳細を取得するケース`
![スクリーンショット_2022-05-18_12_47_51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27785/47915424-01b2-3cc4-b847-8fe47cf7cf09.png)


* 処理は上から順番に実行されます。

    * User一覧の取得が終わってから、その値を用いてUser詳細を取得することができます。



`sleepメソッドのfinishがコンソールに出力されるまで3秒かかるサンプル`
![スクリーンショット_2022-05-18_12_50_56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27785/2dc04881-965d-baa4-3871-dbd405174023.png)

* このように直列実行のため`sleep()`メソッドの全体の実行時間は3秒かかります。


## 並列実行
* 並列実行の方法は`async let`と`with(Throwing)TaskGroup(of:`を用いた書き方があります

### async letパターン
`e.g. 複数のAPIを叩いてそれぞれの結果を合成したモデルを返すケース`
![スクリーンショット_2022-05-18_12_59_07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27785/90047ab1-ce44-93b8-bbfa-4cec1b684554.png)

* `async let`の宣言部では`await`キーワードは不要で、その値を実際に使いたい時に`await`を記述します。

  * また、`async let`で宣言した瞬間に非同期処理が走ります。

```swift
// 非同期処理が宣言とともに走る
async let taskA = fetchA()
async let taskB = fetchB()
// 上記で宣言したtaskA, taskBの結果を用いる場合にawait構文が必要になる
let (resultA, resultB) = try await (taskA, taskB)

let resultC = try await fetchC(a: resultA, b: resultB)
```

* 下記sleepメソッドの`finish`が出力されるのは並列実行のため2秒です。

```swift
func sleep() async throws {
  async let sleep1: () = Task.sleep(nanoseconds: UInt64(1 * 1_000_000_000)) // 1秒
  async let sleep2: () = Task.sleep(nanoseconds: UInt64(2 * 1_000_000_000)) // 2秒
  _ = try await (sleep1, sleep2) 
  print("finish")
}
```

* 注意点として下記のような書き方もできますが、これは並列にならず直列実行になってしまいます。

```swift
async let (taskA, taskB) = (fetchA(), fetchB())
let (resultA, resultB) = try await (taskA, taskB)
```

* また、`async let`で宣言した変数を使うときに`await`を書きますが、複数回その値を使いたい場合に毎度`await`を書いても非同期処理そのものは宣言した瞬間に一度走るだけなので`await`を書くたびに非同期処理が走ることはありません。

```swift
async let value = fetchA()
// 特にfetchA()の処理が２回走ってしまうことはない
let result1 = fetchResult1(value: await value)
let result2 = fetchResult2(value: await value)
```

##### async letな変数を使わずにスコープを抜けた場合
* あるfunction内で宣言した`async let`な変数を`try await`などせずにそのfunctionのスコープを抜けた場合にどのような挙動になるでしょうか。

* 答えはスコープを抜けた時点でまだ`async let`で宣言した処理が完了していなかった場合は、その処理がキャンセルされます。

```swift
func sleepTask() async throws {
    print("sleep start")
    do {
        try await Task.sleep(nanoseconds: 2_000_000_000) // 2秒待つ処理
    } catch {
        print(error.localizedDescription)
    }
    print("sleep finish")
}

func asyncSleep() async throws -> String {
    print("start asyncSleep")
    async let sleep1 = sleepTask()
    async let sleep2 = sleepTask()
    print("end asyncSleep")
    return "finish"
}

// 呼び出しもと
Task {
    do {
        let result = try await asyncSleep()
        print("result: " + result)
    } catch {
        print(error.localizedDescription)
    }
}
```

**コンソールの出力結果**
```
start asyncSleep
end asyncSleep
sleep start
sleep start
The operation couldn’t be completed. (Swift.CancellationError error 1.)
sleep finish
The operation couldn’t be completed. (Swift.CancellationError error 1.)
sleep finish

result: finish
```

* なぜこういった挙動になるのかというと、`async let`で宣言したものは後述する[構造化されたTask](https://qiita.com/hachinobu/items/a892c68a97d38ca1b524#%E6%A7%8B%E9%80%A0%E5%8C%96%E3%81%95%E3%82%8C%E3%81%9Ftask)として扱われるからだと思われます。

    * スコープを抜けても処理を中断するのではなく、そのまま処理を続けさせたい場合は`async let`ではなく、新規で`Task {}`を作り[構造化されていないTask](https://qiita.com/hachinobu/items/a892c68a97d38ca1b524#%E6%A7%8B%E9%80%A0%E5%8C%96%E3%81%95%E3%82%8C%E3%81%A6%E3%81%84%E3%81%AA%E3%81%84task)として扱う必要があります。
    

### with（Throwing)TaskGroupパターン
`e.g. あるAPIの返り値である可変長配列をもとにその数分の並列処理をしたいケース`
    

* APIからユーザーidの一覧を取得して、そのユーザーidをキーに全ユーザー分の詳細情報を取得したい場合
![スクリーンショット_2022-05-18_13_10_24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27785/1b054d9c-503d-f1f0-80cc-7d5a40c81e30.png)

#### withThrowingTaskGroup(of:returning:body:)

1. 第一引数にはgroup.addで追加した子タスクの処理の返り値の型を指定します。
    * `UserDetail.self`

2. 第二引数にはwithThrowingTaskGroup関数自体が返す戻り値の型です。
    * [UserDetail].selfだが書き方によって省略可能

3. 第三引数には子タスクを使った並列処理のクロージャを書きます。
    * クロージャにThrowingTaskGroup型の引数（group)が渡ってくるので、このgroupに並列処理を追加していくことになります。


* **なお、子タスク内の処理でエラーが起きてもその時点ではエラーをthrowしないが、groupに対して`reduce`, `waitForAll()`, `next()`などを呼び出した際にエラーがrethrowされるようになっているので子タスクのエラーハンドリングをしたい場合は注意が必要です。**

    * ![スクリーンショット_2022-05-17_18_42_52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27785/bfc04dd4-7d6c-5a28-02bc-5334f591eda9.png)

* 子タスク内で戻り値がVoid型であるAPIなどを叩いていて、特にgroupの処理を待っていないコードだと子タスクのAPIのエラーが伝播されません。

  
* `withThrowingTaskGroup`のクロージャの引数であるgroupにaddメソッドで子タスクを追加して並列処理を走らせることが可能だが、これはgroupに追加された子タスクがそれぞれ並列で処理されるだけなので子タスクの中で直列に書いたものは直列実行されます。

```swift
let number = try await withThrowingTaskGroup(of: Int.self) { group in
  [1, 2, 3].forEach { num in
    // 最終的に３つの子タスクが作られる
    // 子タスク内の処理は直列で書かれているので、それぞれの子タスクの完了時間は6秒となる
　　 // 子タスク同士は並列に動作するので全ての子タスクが完了する時間も6秒
    group.add {
      _ = try await Task.sleep(UInt64(1 * 1_000_000_000))
      _ = try await Task.sleep(UInt64(2 * 1_000_000_000))
      _ = try await Task.sleep(UInt64(3 * 1_000_000_000))
      return num
    }
  }
  return try await group.reduce(into: 0) { result, num in
    result += num // 6
  }
}
```

* 上記の子タスクが全て終わる時間を3秒にしたい場合は子タスク内の処理を`async let`で記述すれば良いです。

```swift
    group.add {
      async let task1 = try await Task.sleep(UInt64(1 * 1_000_000_000))
      async let task2 = try await Task.sleep(UInt64(2 * 1_000_000_000))
      async let task3 = try await Task.sleep(UInt64(3 * 1_000_000_000))
      _ = try await (task1, task2, task3)
      return num
    }
```

* なお、Errorをthrowしないバージョンの`withTaskGroup(of:returning:body:)`も存在します。

# AsyncStream(AsyncThrowingStream)

* SwiftではSequenceプロトコルに準拠することで`forEach`, `map`, `filter`, `reduce`などの関数や `for-in`文も使えるようになりますが、これの`async/await`に対応した非同期バージョンとしてAsyncSequenceプロトコルというものがあります。

```swift
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
@rethrows public protocol AsyncSequence {

    /// The type of asynchronous iterator that produces elements of this
    /// asynchronous sequence.
    associatedtype AsyncIterator : AsyncIteratorProtocol

    /// The type of element produced by this asynchronous sequence.
    associatedtype Element where Self.Element == Self.AsyncIterator.Element

    /// Creates the asynchronous iterator that produces elements of this
    /// asynchronous sequence.
    ///
    /// - Returns: An instance of the `AsyncIterator` type used to produce
    /// elements of the asynchronous sequence.
    func makeAsyncIterator() -> Self.AsyncIterator
}
```


* 例としてURL型にはlinesというプロパティが生えていて、このプロパティの型はAsyncSequenceプロトコルに準拠したAsyncLineSequenceという型が定義されています。

```swift
public var lines: AsyncLineSequence<URL.AsyncBytes> { get }
```

* これを使うことで指定したURLからその内容を非同期で1行ずつ取得することが可能になります。

```swift
Task {
    let url = URL(string: "https://www.apple.com/jp/")!
    for try await line in url.lines {
        print(line + "🌟")
    }
}
```

* 出力

```
<!DOCTYPE html>🌟
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja-JP" lang="ja-JP" prefix="og: http://ogp.me/ns#" class="no-js" data-layout-name="apple-trade-in-event">🌟
<head>🌟
	🌟
<meta charset="utf-8" />🌟
<link rel="canonical" href="https://www.apple.com/jp/" />🌟
	🌟
```

* このようにAsyncSequenceプロトコルに準拠することで非同期処理の1つ1つが完了したタイミングで値を取得でき、`map`や`reduce`などを使ってその値を簡単に変換することが可能になります。

* AsyncStreamを使うとカスタムな型をAsyncSequenceプロトコルに容易に準拠させることが可能です。

![スクリーンショット_2022-05-18_13_25_48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27785/0f4f6fc7-6eb9-9db6-6179-d50030ffc6c4.png)


* このようにAsyncStreamでラップすることでAsyncSequenceプロトコルに準拠したものを簡単に作ることができる

* AsyncStreamのinitのクロージャにContinuationという型の引数(continuation)が渡ってくるので、continuationに対して`yield(_ value:)`もしくは`finish()`を流します。

    * `yield(_ value:)`にはAsyncStreamのジェネリクスで定義した型の値を流します。
    
    * continuationにErrorを流すことのできる`finish(throwing: )`はAsyncThrowingStreamを使うことで可能です。

* AsyncStreamに`finish()` or `finish(throwing: )`が流れた時点でStreamが終了します。

    * AsyncStreamをfor-loopなどで使っていた側のloop処理を抜けるということです。
    
    * 逆にいうと`finish()`or`finish(throwing: )`を流さない場合、呼び出し側のfor-loopが終わらないので注意が必要となります。
    
    * AsyncStreamを実行しているTaskをキャンセルしてもfor-loopを抜けることができるが、その後にキャンセルをハンドリングして適切な`finish(throwing: )`を呼ばないとAsyncStreamを実行しているTask自体が正常終了と見なされてしまいます。（Task章で後述）

* また、特にAsyncStream内の処理が並列に実行されるわけではありません。
![スクリーンショット_2022-05-18_13_30_23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27785/dce04e12-6353-4609-0153-b634648b6ef6.png)


* AsyncStreamを並列に実行したい場合はクロージャ内で並列実行のコードを書けば良いだけです。
![スクリーンショット_2022-05-18_13_32_44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27785/2306bcbd-8c2b-abe2-fb5b-aef879642f47.png)




### onTermination
* AsyncStreamのonTerminationを使うとStreamが何によって終了したのか検知することができます。

    * 主に`正常/エラー`終了時やキャンセル時などの後処理を記述できます。
![スクリーンショット_2022-05-18_13_40_59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27785/3cc399d4-211f-dc1a-ba71-0dbb352053ba.png)


### AsyncStreamはダウンロードプログレスのような処理に最適

* AsyncSequenceの特徴として非同期処理の1つ1つが完了したタイミングで通知がきて、エラーが起きると以降の処理は通知されません。

* この特性はダウンロードの完了までにプログレス表示をするようなケースに使えるものです。

* 例えば既存のAPIでファイルをダウンロードするような下記のFileDownloaderというコードがあるとします。

![スクリーンショット_2022-05-18_13_48_11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27785/978bb9de-3cad-22d4-278b-a066addd70ac.png)


* この既存APIをAsyncStreamでラップすることで下記のように定義できます。
![スクリーンショット_2022-05-18_13_53_01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27785/ef0533c0-e21f-73bb-c765-b3b01ac3b8bb.png)


`呼び出し側`
![スクリーンショット_2022-05-18_13_54_29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27785/7a5484d9-e148-f564-0144-3bb84572bdcc.png)


# Taskについて

* **Task型**
  * **`@frozen struct Task<Success, Failure> where Success : Sendable, Failure : Error`**

    * **イニシャライザ**
      * `@discardableResult init(priority: TaskPriority? = nil, operation: @escaping @Sendable () async -> Success)`

```swift
func createTask() -> Task<Int, Error> {
  Task {
    try await Task.sleep(nanoseconds: UInt64(1 * 1_000_000_000))
    return 1
  }
}
```

* ジェネリクスで定義した`Success, Failure`型の値を保持します。

    * クロージャ内でその型を返します。

    * `try await`の箇所でErrorがthrowされた場合、Taskの`Failure`型の方にそのエラーが代入されます。

### 当初のTaskの役割やイメージ

* `async`付きのメソッドを並行性をサポートしていない同期環境から呼び出そうとしたときに出るエラーで`'async' in a function that does not support concurrency`というエラーが出ます。

![スクリーンショット 2022-05-16 22.52.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27785/3d07036f-172c-c6b2-805f-04cd6187d2c0.png)
 
* これを回避するには同じく`async`付きのメソッドから呼び出すか`Task {}`で囲む必要があります。
    
    * アプリケーションの根源は同期環境から始まるので`async`なメソッドを呼び出す際にどこかで必ずTaskを使う必要があります。

* なので手っ取り早く`'async' in a function that does not support concurrency`を回避できるもの・・

    * (^p^)<なんかよく分からないけどTaskで囲んだらなおった・・
    * という程度の認識でした。
 
![スクリーンショット 2022-05-16 22.53.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27785/fc0ef1b6-fa10-201a-0509-7b1912c82f05.png)


* ここからもう一歩理解を進めてみました。



### Taskとは何なのか

* タスクとはプログラムの一部として非同期で実行できる作業の単位であり、すべての非同期コードは何らかのタスクの一部として実行されます。

![スクリーンショット_2022-05-17_11_40_08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27785/d3a5bb80-e2b2-4706-f732-972552b456b5.png)

1. 2つのTaskを作って、そのTask内で非同期のsleepを呼び出すと各々のTaskが並列に実行されます。

2. メソッド内の順序としては出力の内容の通りでTaskの非同期処理が終わる前にこのメソッドを抜けます。

3. 各Taskは並列に実行されるので2秒後に最後の非同期処理の`finish sleep 2 second`が出力されます。

4. Taskは生成後すぐに実行され明示的な開始の必要はありません。

    i. ただしTaskのハンドリングをしないと処理を非同期で投げっぱなしただけになるのであまり使い所はない気がします。
(RxSwiftのdoオペレータのように成功・失敗関係なくとりあえず非同期にログを送っておくみたいな処理には良いかもしれませんが)

### Taskのハンドリング

* では下記のような3つのTaskの非同期処理が完了してから関数の返り値であるInt型を返すようにするにはどうすれば良いでしょうか。

![スクリーンショット 2022-05-17 14.12.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27785/58d3cfd8-f87f-e8dd-6d3f-1072af81d0eb.png)

* 出力の通りこの書き方だと関数を抜けて`3`が出力されてから各々の非同期処理が完了しています。



1. 私が最初に思いついたのは、この関数を`async`にして呼び出し側に`await`を書くことでした。

    1-2. しかしながらこれでは先ほどと同じ結果になってしまいました。

##### Taskで実行した非同期処理の値を取得できる`result`と`value`プロパティ
* 関数を各Taskの非同期処理が終わってからInt型を返すようにしたい場合は、Task型に生えている`result`もしくは`value`というプロパティが`async`になっているのでこれを使うことで非同期処理の完了を待つことができます。

    * `result`プロパティはResult型になっていてSuccessもしくはErrorの値が取得できます。

    * `value`プロパティはジェネリクスで定義したSuccess型の値の取得を試みますがTaskがエラーをスローした場合、このプロパティはそのエラーを伝搬します。


```swift
public var result: Result<Success, Failure> { get async }

public var value: Success { get async throws }
```

* **`result`プロパティを使って非同期処理の完了を待ってから関数の返り値を返すようにできる**
![スクリーンショット 2022-05-17 15.16.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27785/2e9fb380-7607-23f9-3478-275389be81aa.png)

* 出力を見てみると

    1. 全ての非同期Taskが終わってから`3`が返されるようになっています。
    
    2. 各Taskが並列ではなく直列で実行されるようになっているので`Task.sleep`コードが上から2秒、1秒、3秒の順で終わり全てのTaskの処理が完了するまで合計で6秒かかるようになっています。
     
    
* **`value`プロパティを用いた場合はErrorがthrowされる可能性があるので下記のようになる**
![スクリーンショット_2022-05-17_15_27_37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27785/e80d483e-0d51-4356-957f-a65c37633420.png)


* **各Taskを並列に実行した上で完了を待ちたい場合**

* 元々Taskは宣言した瞬間に実行されるので各Taskを変数に保持してタプルで一気に`try await`してあげると並列になります。

    * 下記のように関数の完了は3秒になります。
![スクリーンショット_2022-05-18_1_17_11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27785/1e5b7cec-5948-48b0-8e84-ea6ab46b61be.png)


* または、各Taskの`result`を`async let`で宣言して、`await`することで並列実行になる

    * 関数の完了は3秒になる

![スクリーンショット_2022-05-17_15_44_22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27785/d7a4cbed-b195-3e6d-d4c7-9fcaaf7d7ca6.png)

### Taskのエラーハンドリング
* Taskの`result`プロパティには非同期処理の結果が格納されます(`Success` or `Failure`)。

* Task内の非同期処理でErrorがthrowされた場合は下記のようにエラーハンドリングできます。

![スクリーンショット 2022-05-17 16.04.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27785/c14ba13b-8165-f872-19de-c2e520a9f53a.png)

* **`value`プロパティを使った呼び出しの場合**

![スクリーンショット 2022-05-17 16.06.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27785/1aa66d94-a03d-7374-dfbe-e6a4b68aecca.png)

#### 複数のTaskでエラーが発生した際にどのエラーがハンドリングされるのか

* 書き方(`value`, `result`, `try?`など)によるが直列と並列のシンプルなパターンでの順番を記載します。

* **直列の場合**

![スクリーンショット 2022-05-17 16.57.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27785/86dd847a-f745-3351-27e4-f20cb0b737cc.png)

* 各Taskを直列に実行した場合、最初にエラーが発生したTaskのErrorがthrowされ、次のTaskは処理されません。

* **並列の場合**

    * `async let`を使った場合、直列と同じように`try await`を記述した順番でthrowされるエラーが決まる
    
        * ![スクリーンショット_2022-05-17_17_48_23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27785/8d3df448-b1b7-da2c-6009-145d4cbbae19.png)
    
    * `async let`で宣言した処理を`try await`でタプルで書いた場合、エラーがthrowされた順番ではなく常に左辺のエラーが優先されます。
    
        * ![スクリーンショット_2022-05-17_17_34_27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27785/6a6a07aa-b240-fdbb-99d8-f910aaddc5ce.png)
    * Taskをタプルで待ち合わせた場合も同じなのでタプルは左辺が優先されるっぽい
        * ![スクリーンショット_2022-05-18_1_29_54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27785/bd1d178e-b9ce-86e4-b1d8-3356f4454e4d.png)

        
    * `withThrowingTaskGroup(of:`を使った場合は最初にエラーが発生した方が適用されます。
    
        * ![スクリーンショット_2022-05-17_18_13_22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27785/ba3b8948-9852-d592-7784-fc92cc5e06c8.png)

    * 全てErrorがthrowされるTaskであっても並列処理の場合は全ての非同期処理が走り続け、各々が完了するまで止まりません。

### Taskのキャンセル処理

* Taskには`cancel()`メソッドがありTaskの処理を停止することができます。

    * 標準APIであるTaskのsleepメソッドはスリープ中にcancel()が実行されると`CancellationError`をthrowするような設計になっています。

```swift
public static func sleep(nanoseconds duration: UInt64) async throws
```

* このメソッドを使ってキャンセル処理をすると下記のようになります。

![スクリーンショット_2022-05-17_19_30_10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27785/c2257160-3643-d7d5-d2fb-0a00efc8491a.png)

* 同じようにネットワークリクエストで`public func data(from url: URL, delegate: URLSessionTaskDelegate? = nil) async throws -> (Data, URLResponse)`を実行中に`cancel()`を使うとNSURLErrorDomainのcancelledがthrowされているようになっています。

    * ただしタスクをキャンセルしても、その場で即座に処理が停止するわけではありません。
    
    * タスクのキャンセルはタスクに "キャンセルされた" というフラグを立てるだけで、本当にキャンセル実行するのかどうかは各タスク側で明示的に確認する必要があるみたいです。
    
    * この仕様を 「協調的なキャンセル」（Cooperative cancellation）というらしいです。 

![スクリーンショット_2022-05-17_21_43_20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27785/0ee50f95-fa8e-9ffe-6bc1-bbb3b0fe7192.png)


* このようにTaskがキャンセルされた時にどういった処理をするかということを自前の非同期処理に実装することができます。

##### Taskのキャンセルを検知する
* 自身の非同期処理(Task)のキャンセルを検知するAPIは下記があります。

```swift
public var isCancelled: Bool { get }

public static var isCancelled: Bool { get }

public static func checkCancellation() throws

public func withTaskCancellationHandler<T>(operation: () async throws -> T, 
                                           onCancel handler: @Sendable () -> Void) async rethrows -> T

public func withTaskCancellationHandler<T>(handler: @Sendable () -> Void, 
                                           operation: () async throws -> T) async rethrows -> T
```


* **例えばあるファイルをネットワークからダウンロードして、そのデータをさら別の形式に変換する重い処理をするTaskがあったとして、これらのAPIを使ってキャンセルチェックを行うことで不要な処理を減らせるようになります。**

###### `static var isCancelled: Bool { get }`
* Taskがキャンセルされたかどうかを返します。

    * staticプロパティはTaskのクロージャ内でのみ自身がキャンセルされたかどうかを判定できます。
    
* 下記はファイルダウンロード後にキャンセルチェックをしてキャンセルされていた場合は独自定義のErrorをthrowする場合のコードです。
    
![スクリーンショット_2022-05-17_22_13_13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27785/ec3c3926-f2f7-50f4-08a1-e288440525ef.png)

* これにより不要な重い処理を回避することができます。


###### `public static func checkCancellation() throws`
* Taskがキャンセルされていた場合に`CancellationError`をthrowするメソッドです。

![スクリーンショット_2022-05-17_22_10_18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27785/f9c41ac7-0b99-b635-5754-d770c269e8cb.png)


###### public func withTaskCancellationHandler<T>(operation: () async throws -> T, onCancel handler: @Sendable () -> Void) async rethrows -> T

* これまではキャンセルを自ら定期的にチェックする方法でしたが、`withTaskCancellationHandler`を使うことでキャンセルされたタイミングを即座に検知することができます。

    * ただし、**キャンセルを即座に検知できるだけで`operation:`クロージャに書いた処理は中断されることなく続くので、キャンセルを即座に検知した際に自前で中断できるような書き方をする必要があります。**


![スクリーンショット_2022-05-17_22_29_06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27785/2d4a15da-0546-bc83-0412-399a4ceddaf9.png)


* `public func withTaskCancellationHandler<T>(handler: @Sendable () -> Void, operation: () async throws -> T) async rethrows -> T` は`operation:`と`onCancel handler:`の引数の位置が逆になっただけのものなので割愛




### 構造化されたTask
* 各々のTaskのライフサイクルやキャンセルが管理されている構造のこと(親子関係)です。

    * 構造化されていることで親Taskの`cancel()`メソッドを呼び出すと、そのTaskに関連する子Taskのキャンセルも自動で呼び出されます。
    
    * 構造化されたTaskを作れるのは`async let`と`with(Throwing)TaskGroup`を使った場合のみです。
    
* 下記のように親タスクの`cancel()`を呼び出すことで`async let`で内部的に作られた子タスクにもキャンセルが伝播し`try await URLSession.shared.data(from:`がキャンセル時にthrowするNSURLErrorDomainのcancelledが出力されていることが確認できます。
![スクリーンショット_2022-05-18_0_45_56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27785/5f67cc0a-f049-d38b-34d4-1d28c74c36d3.png)



* `withTaskGroup`バージョン
![スクリーンショット_2022-05-18_0_54_33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27785/fb441107-be4b-a640-45df-541713feda74.png)



### 構造化されていないTask
* 各々のTaskが独立しておりライフサイクルやキャンセルが共有されていない構造のことです。

    * 関連性がないのであるTaskの`cancel()`メソッドを呼び出しても他のTaskに通知がいくことはありません。
    
    * Task型のイニシャライザや`Task.detached`などを用いて作成したものは全て構造化されていないTaskになります。（`async let`や`with(Throwing)TaskGroup`以外）

* Taskの内部で新たにTaskを生成してもインナーのTaskの処理まではキャンセルが伝播しません。
![スクリーンショット_2022-05-18_1_07_41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27785/b0649ce2-b345-77af-c632-d71940a59ecc.png)

* キャンセルされたTask側で検知してインナーのTaskもキャンセルするようなコードを書くことで対応する必要があります。
![スクリーンショット_2022-05-18_1_41_14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27785/cf9bc88b-2cb1-2679-a6ae-a858b214c2f8.png)


* `async let`を使っても新たなTaskを生成しているような場合は構造化された関係にはなりません。
    * 新たにTaskを生成した時点でその処理は構造化されていないタスクになる
![スクリーンショット_2022-05-18_14_41_23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27785/0c3bcb72-fa1d-69db-41d7-cb614c60f4bc.png)


##### Async(Throwing)Streamの場合のキャンセル処理

* AsyncStreamを実行しているTaskがキャンセルされるとStreamが終了する(for-loopを抜ける)が、`onTermination`クロージャの中でキャンセルを検知して明示的にstreamに`finish(throwing: )`を流さないとエラーが呼び出し元にthrowされません。
![スクリーンショット_2022-05-18_11_40_52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27785/d2ca96bd-dcb2-6768-3955-35b5225e23ee.png)

* onTerminationでキャンセルを検知してStreamに`finish(throwing: )`を流した場合
![スクリーンショット_2022-05-18_11_44_16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27785/0697612e-4c1f-17d7-c1b9-3d477349d72c.png)

    * これによりTaskのresultもfailureになりStream側でthrowしたエラーになっていることが確認できます。

###### Async(Throwing)Streamクロージャ内で定義しているTaskについて
* AsyncStreamのイニシャライザでstreamに値を流すクロージャは`async`なクロージャになっていないので、非同期処理を呼び出す時にはTaskを使う必要があります。

* これはAsyncStreamを使う外側のTaskとAsyncStream内の非同期処理のTaskが構造化されていないことを表しています。

`AsyncStream内のTask(非同期処理)が処理され続ける例`
![スクリーンショット_2022-05-18_12_15_55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27785/8db92198-b6e7-278e-55d4-2b1c424932fe.png)


* AsyncStreamを使う側のtaskをキャンセルしているのでStreamに対しての`continuation.yield("a")`などの値は流れてきませんが、コンソールには`finish sleep 1`などが出続けることからAsyncStream内のTask処理がキャンセルされていないことが分かります。

  * このマズさはダウンロードをキャンセルする場合などを想像してみると明白です。


* これを回避するにはStreamの`onTermination`でキャンセルを検知し、AsyncStream内で定義したTaskのインスタンスに対してキャンセルを実行する必要があります。

`AsyncStream内のTaskもキャンセルする例`
![スクリーンショット_2022-05-18_12_25_56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27785/3eb63744-47e9-d306-af43-cd987be94650.png)

* `innerStreamTask`をキャンセルすることでコンソールに`finish sleep 1`などが出なくなりました。

    * `innerStreamTask`の`Task.sleep`がキャンセルを検知したことにより`CancellationError`がthrowされたためです。
    
        * なお、throwされたCancellationErrorは `innerStreamTask`のresultに格納されますが、このエラーがAsyncStreamの呼び出し元に伝搬されることはありません。（呼び出し元に伝わるのはコンソールに表示されている通り`continuation.finish(throwing: MyError.e1)`です。）


* 一連の理解を経て私は、**Taskのキャンセルを意識した設計がかなり重要**なんだなと感じました。

    * asyncなメソッドはTaskを作らずにasyncな処理を呼び出せるので、意図せず`構造化されていないTask`を作ってしまうことを回避できる役割があるのだなとも思いました。


# まとめ
いかがでしたでしょうか。
まだまだ調べきれていないことや説明できていない機能が盛りだくさん（Actorについて一切触れておらず）ですが、この備忘録程度の記事が何かのお役に立てれば幸いです。


# 参考にさせていただいたドキュメントや記事など
* https://zenn.dev/akkyie/articles/swift-concurrency
* https://www.swiftbysundell.com/articles/the-role-tasks-play-in-swift-concurrency/
* https://www.avanderlee.com/concurrency/tasks/
* https://www.swiftbysundell.com/articles/swift-concurrency-multiple-tasks-in-parallel/
* https://www.swiftbysundell.com/articles/async-sequences-streams-and-combine/
* https://www.donnywals.com/understanding-swifts-asyncsequence/
* https://www.avanderlee.com/swift/asyncthrowingstream-asyncstream/
* https://developer.apple.com/documentation/swift/swift_standard_library/concurrency