Closed KaitoMuraoka closed 8 months ago
また、コールバックの呼び出しは全てのパスで確実に呼ばれることを想定しているが、実際に呼び出すかは開発者の責任になる→バグの発生原因になる
Q.待機可能なプログラムとは? A.
上記のように処理は同意的なコードと同じように上から下に流れていくため、読みやすいコードになっている。
また、非同期関数やメソッドは並行処理のための特別なコンテキストで、実行が必要である(本書では「非同期コンテキスト」と呼ぶ)
await: プログラムにそのメソッド・プロパティが待機可能であることを伝える。メソッドやプロパティが待機状態になる
SwiftConcurrency では、実行中のメソッドやプロパティを中断・再開して非同期処理を行う
同期関数・メソッドを非同期関数にするには引数の括弧の後ろになる戻り値の矢印(-> ) に 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 なしで呼ぶとコンパイルエラーとなります。
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 を返すことです。
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
を使う。
使い方は、ほぼ同じ
// コールバック形式の関数
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 メソッドを呼び出し忘れるとエラーになる。 また、2回以上でもエラーになるので、 必ず1回実行する
This issue is stale because it has been open for 30 days with no activity.
This issue was closed because it has been inactive for 14 days since being marked as stale.
本題
そろそろSwift Concurrencyを入門しないとダメだなと クロージャに頼って非同期処理しているのもダメだなと思ったのでやってみます。