KaitoMuraoka / Qiita-contents

Qiita記事とそのネタを管理するリポジトリ
1 stars 0 forks source link

Swift Concurrency 入門を読んでみる #63

Closed KaitoMuraoka closed 8 months ago

KaitoMuraoka commented 9 months ago

本題

そろそろSwift Concurrencyを入門しないとダメだなと クロージャに頼って非同期処理しているのもダメだなと思ったのでやってみます。

KaitoMuraoka commented 9 months ago

第1章 async / await

https://github.com/SatoTakeshiX/first-step-swift-concurrency/blob/ab635d04acb55df1c7d4d2633b8cc9cb9fbbfa76/try-concurrency.playground/Pages/1-1-request-with-closure.xcplaygroundpage/Contents.swift#L16-L27

また、コールバックの呼び出しは全てのパスで確実に呼ばれることを想定しているが、実際に呼び出すかは開発者の責任になる→バグの発生原因になる

async/await で解決する

Q.待機可能なプログラムとは? A.

https://github.com/SatoTakeshiX/first-step-swift-concurrency/blob/ab635d04acb55df1c7d4d2633b8cc9cb9fbbfa76/try-concurrency.playground/Pages/1-2-request-with-async.xcplaygroundpage/Contents.swift#L20-L25

上記のように処理は同意的なコードと同じように上から下に流れていくため、読みやすいコードになっている。

また、非同期関数やメソッドは並行処理のための特別なコンテキストで、実行が必要である(本書では「非同期コンテキスト」と呼ぶ)

https://github.com/SatoTakeshiX/first-step-swift-concurrency/blob/ab635d04acb55df1c7d4d2633b8cc9cb9fbbfa76/try-concurrency.playground/Pages/1-2-request-with-async.xcplaygroundpage/Contents.swift#L46-L56

プログラムの待機可能性とは?

await: プログラムにそのメソッド・プロパティが待機可能であることを伝える。メソッドやプロパティが待機状態になる

SwiftConcurrency では、実行中のメソッドやプロパティを中断・再開して非同期処理を行う

async/await の文法

非同期関数を定義する

同期関数・メソッドを非同期関数にするには引数の括弧の後ろになる戻り値の矢印(-> ) に async をつける

// 戻り値がない場合
func a() async {
    print(#function)
}

Task.detached {
    await a()
}

// 戻り値がある場合
func b() async -> String {
    return "result"
}

Task.detached {
    let result = b()
    print(result)
}

エラーが発生する場合は、非同期関数に throws の前に async をつける。 呼び出す際には try を使う

Task.detached {
    do {
        try await c(showError: true)
    } catch {
        print("error")
    }
}

イニシャライザ

イニシャライザーにも async キーワードをつけることができる。呼び出しもとはインスタンス生成時に await キーワードをつける。

    class D {
        init(label: String) async {
            print("イニシャライザーで async")
        }
    }

    Task.detached {
        _ = await D(label: "")
    }

非同期関数を await なしで呼ぶとコンパイルエラーとなります。

複数の非同期関数を1つの await にまとめる

await は複数の非同期関数やプロパティがある場合、1つにまとめることができる。 await キーワードを2回呼び出す場合:

Task.detached {
    let result = await b()
    let d = await D(label: result)
    print(d)
}

これを1つにまとめることができる

Task.detached {
    let d = await D(label: b())
    print(d)
}
1行で記述すると、await  が1つだけでもそれぞれが待機するコードになります。

## 順列実行と並列実行
非同期関数で複数で順番に実行したい場合は、単純に `await` キーワードをつけて関数を順々に呼び出せば良い。
非同期関数で順列実行:
```swift
func runAsSequence() async {
    await waitOneSecond()
    await waitOneSecond()
    await waitOneSecond()
}

これが順列実行

一方で、並列的に処理を行いたい場合があります。同期関数で並列的に処理を行いたい場合は、 `DispatchGroup' が利用できます。

同期関数で並列処理

func funAsParallel(completionHander: @escaping (() -> Void)) {
    let group: DispatchGroup = .init()
    group.enter()
    waitOneSecond {
        group.leave()
    }

    group.enter()
    waitOneSecond {
        group.leave()
    }

    group.enter()
    waitOneSecond() {
        group.leave()
    }

    group.notify(queue: .global()) {
        completionHander()
    }
}

非同期処理を実行する前に enter メソッドを呼び出し、非同期処理が終わったら leave メソッドを呼び出しています。すべての非同期処理が終わると notify メソッドのコールバックが呼ばれるので、すべての処理が終わったら行う処理を記述する流れです。 DispatchGroup のおかげで同期関数でも並列処理を実装できます。しかし、ボイラープレートが多く、 enter メソッドと leave メソッドを確実に行うのは開発者の責任になります。

では、非同期処理では並列処理はどのように表現できるでしょうか。 async let バイディングで、非同期関数を並列で呼び出すことができます。

import Foundation

private func waitOneSecond() {
    DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
        print("wait")
    }
}

func funAsParallel(completionHander: @escaping (() -> Void)) {
    async let fist: Void = waitOneSecond()
    async let second: Void = waitOneSecond()
    async let third: Void = waitOneSecond()

    await fist
    await second
    await third
}

非同期処理の戻り値を async let で変数定義をすると、プログラムはそのメソッドの完了を待たずに次の行へ移る。そして、その変数を利用するところで、変数に await をつけることで、その処理が終わるまでプログラムは中断される。

これで、非同期関数における並列処理が表現できる。

クロージャ形式のメソッドを非同期関数にラップする

クロージャ形式で、非同期処理を行うメソッドを非同期関数に変換できると便利でです。 Swiftは標準ライブラリに、withChsckedContinuation 関数と withCheckedThrowingContinuation 関数です。これらの関数を使うことで、クロージャ形式で実装された同期関数を非同期関数にラップすることができます。

withCheckedContinuation

同期関数をラップする際に、エラーをスローしない非同期関数を作成したい場合は、withCheckedContinuation 関数を使います。使い方は、ラップする非同期関数を新しく定義し、その戻り値として withCheckedContinuation を返すことです。

struct User{}

// ラップしたい同期関数
func fetchUser(userID: String, completionHandler: @escaping ((User?) -> ())) {
    if userID.isEmpty {
        completionHandler(nil)
    } else {
        completionHandler(nil)
    }
}

// ラップする非同期関数
func newAsyncFetchUser(userID: String) async -> User? {
    return await withCheckedContinuation { continuation in
        fetchUser(userID: userID) { user in
            continuation.resume(returning: user)
        }
    }
}

newAsyncFetchUser 関数が新しくラップする非同期関数です。戻り値として withCheckedContinuation を呼び出します。 withCheckedContinuation のクロージャから取得できる continuation は、CheckedContinuation 型で、同期関数と非同期関数の橋渡しするものです。 withCheckedContinuation のクロージャ内でラップ対象の関数を呼び出して、ラップ対象の関数・メソッドのクロージャ内で continuation.resume メソッドを実行する。resumeメソッドには、ラップしたい関数のコールバックの引数を渡す。

newAsyncFetchUser 関数の呼び出しは以下の通り: ``swift func main() { Task.detached { let userID = "1234" let user = await newAsyncFetchUser(userID: userID) print(user ?? "")

    let noUser = await newAsyncFetchUser(userID: "")
    print(noUser ?? "no user")
}

}


全文コードは以下の通り:
```swift
import Foundation

struct User{}

// ラップしたい同期関数
func fetchUser(userID: String, completionHandler: @escaping ((User?) -> ())) {
    if userID.isEmpty {
        completionHandler(nil)
    } else {
        completionHandler(nil)
    }
}

// ラップする非同期関数
func newAsyncFetchUser(userID: String) async -> User? {
    return await withCheckedContinuation { continuation in
        fetchUser(userID: userID) { user in
            continuation.resume(returning: user)
        }
    }
}

func main() {
    Task.detached {
        let userID = "1234"
        let user = await newAsyncFetchUser(userID: userID)
        print(user ?? "")

        let noUser = await newAsyncFetchUser(userID: "")
        print(noUser ?? "no user")
    }
}

このようにすることで、エラーをスローしない関数・メソッドを非同期関数に変換することができる。

withCheckedThrowingContinuation

同期関数をラップする際に、エラーをスローする非同期関数を作成したい場合は withCheckedThrowingContinuation を使う。 使い方は、ほぼ同じ

// コールバック形式の関数
func request(with urlString: String, completionHandler: @escaping(Result<String, Error>) -> ()) {}

// ラップした async 関数
func newAsyncRequest(with urlString: String) async throws -> String {
    return try await withCheckedThrowingContinuation { continuation in
        // コールバック形式の関数を呼ぶ
        request(with: urlString) { result in
            continuation.resume(with: result)
        }
    }
}

newAsyncRequest が新しくラップする非同期関数。return try await withCheckedThrowingContinuation で、withCheckedThrowingContinuationを呼び出す。 クロージャーの引数 continuation は、CheckedContinuation 型のインスタンスである。withCheckedThrowingContinuatin のクロージャ内でラップ対象の関数を呼び出して、ラップ対象の関数のクロージャ内で continuation.resume メソッドを実行する。resumeメソッドにはラップ対象の関数のクロージャ引数を渡す。

呼び出しは以下の通り:

// 呼び出し
func main() {
    Task.detached {
        let urlString = "https://api.github.com/search/repositories?q=swift"
        // ラップした関数で呼び出す
        let result = try await newAsyncRequest(with: urlString)
        print(result)
    }
}

await キーワードでプログラムを中断・再開するのだが、この「再開」をマニュアルで行うのが resume メソッドと考えれば良い。

CheckedContinuation 型の resume メソッドには主に次の3つのメソッドが良いされている。柔軟にラップする関数のコールバック引数を渡せる

// ラップする関数の正常系の値を引数にとる
resume(returning:)
// ラップする関数のエラーを引数にとる
resume(throwing:)
// Result型を引数にとる
resume(with:)

resume メソッドは必ず1回実行する

resume メソッドを呼び出し忘れるとエラーになる。 また、2回以上でもエラーになるので、 必ず1回実行する

github-actions[bot] commented 8 months ago

This issue is stale because it has been open for 30 days with no activity.

github-actions[bot] commented 8 months ago

This issue was closed because it has been inactive for 14 days since being marked as stale.