今回は非同期処理の方法をサロンの勉強会で理解を深めたので、そのアウトプットをしていきたいと思います。いろんな人から意見をいただけてとても勉強になりました。一人で勉強するよりも、いろんな人と話し合いながら勉強することで、楽しく学べちゃいます!もし、興味あればどうぞ!
🥋アプリ道場サロン🥋
サンプルコードはたくさんの人がスマートにまとめていらしたので、実際にどんな感じで使うのか具体的に載せていけたらなと思い長ったらしいコードを書きました。笑
間違い等があればコメントで教えていただけたら幸いです!!
流れ
- 非同期処理とは?
- キーワード
-
completionクロージャ
を使った非同期処理 -
async/await
を使った非同期処理 -
completionクロージャ
とasync/await
の違いって何? - まとめ
非同期処理とは?
担当直入に言うと、実行結果を後で受け取る。ということだそうです。
日本語版のswiftには以下のように記載してあります。
非同期コード(asynchronous code)は、コードは一度にプログラムの 1 箇所のみで実行されますが、後で中断(suspend)および再開(resume)できます。こうすることで、ネットワーク上のデータの取得やファイルの解析などの長く時間のかかる操作の途中で、UI の更新などの短い時間で完了できる操作を行い、その後引き続き操作を続けることができます
僕の中で同期・非同期をバシッとイメージできた例えを教えていただいたので、イメージできない人は参考にしていただけたらと思います。
(コールセンターの例え)
同期
コールセンターに電話をかけて、問題解決できるまで電話を切らずに待ち続けている状態。(電話を切ることができない。答えを聞くまで何もできずに待つしかない)
非同期
コールセンターに電話をかけて、問題解決に時間がかかるため、折り返し電話を待っている状態。(電話を切って他の作業をすることができる)
今回非同期処理を実現するためにcompletionクロージャ
とasync/await
を使用します。
キーワード
『@escaping
』
『async/await』
completionクロージャを使った非同期処理
これです。正直、クロージャむずいっす。@escaping
をクロージャ引数に持たせます。
@escaping
を引数のクロージャに持たせると、関数の外でも、クロージャの処理を実行することが可能となります。(関数外で実行するのに、@escaping
をつけていないと、エラーが発生します。)
今回はイメージしやすいよう(主観)に文字をデータに見立てたコードを書いてみました。中身は2秒後に大きな画像データを取得し、(省略しましたが)1秒後に小さな画像データを取得するようなコードになってきます。
下の例では、DispatchQueue.global().asyncAfterのクロージャ内で関数の実行が終了した後に、completionクロージャを呼んでいるため、@escaping
が必要となります。
enum FetchImageDataError: Error {
case thumbnailFetchFailed, largeImageFetchFailed, combinedFetchError
}
func fetchLargeImageData(completion: @escaping (Result<String, Error>) -> Void) {
DispatchQueue.global().asyncAfter(deadline: .now() + 2){
completion(.success("LargeImageData"))
}
}
func fetchThumbnailImageData(completion: @escaping (Result<String, Error>) -> Void){
//...上と同じ処理(秒数1)
}
func fetchImageData(completion: @escaping (Result<String, Error>) -> Void) {
fetchThumbnailImageData { thumbnaiImagelResult in
switch thumbnaiImagelResult {
case .success(let thumbnailImage):
fetchLargeImageData { largeImageResult in
switch largeImageResult {
case .success(let largeImage):
let imageData = "小さい画像:\(thumbnailImage)、大きな画像:\(largeImage)"
completion(.success(imageData))
case .failure:
completion(.failure(FetchImageDataError.largeImageFetchFailed))
}
}
case .failure:
completion(.failure(FetchImageDataError.thumbnailFetchFailed))
}
}
print("関数の実行は終了")//⭐️ただクロージャはデータを取得しようと頑張ってくれています。
}
fetchImageData { result in
switch result {
case .success(let imageData):
print(imageData)
case .failure:
print(FetchImageDataError.combinedFetchError)
}
}
実行結果
- 関数の実行は終了
- 小さい画像:ThumbnailImageData、大きな画像:LargeImageData
async/awaitを使った非同期処理
上のコードを愚直に書くとこんな感じのコードになるのかなと思います。
async/awaitで非同期処理を書くためには、非同期であることを示すasync
をパラメータの後に記述します。値を返す場合は、戻り値の型の矢印(->)の前に asyncを記述します。今回はエラーも発生するようなコードになっているのでthrows
も記述しています。throwsの位置はasyncの後です。
実行する場合は、非同期になっているためTask
で囲みます。そして、非同期関数の前にawait
を付け、エラーが発生する可能性もあるため、その前にtry
を付けます。do
で成功パターンをcatch
で発生したエラーの処理を実行します。
func fetchLargeImageData() async throws -> String {
let largeImage:String?
try await Task.sleep(for: .seconds(2.0))//データ取得まで2秒かかる。
largeImage = "largeImage" //データを取れたと仮定
guard let largeImage = largeImage else {
throw FetchImageDataError.largeImageFetchFailed //エラーを投げる
}
return largeImage
}
func fetchThumbnailImageData() async throws -> String {
let thumbnailImage:String?
try await Task.sleep(for: .seconds(1.0))
thumbnailImage = "thumbnailImage"
guard let thumbnailImage = thumbnailImage else {
throw FetchImageDataError.thumbnailFetchFailed
}
return thumbnailImage
}
func fetchImageData() async throws -> String {
do {
let largeImagaData = try await fetchLargeImageData()
let thumbnailImageData = try await fetchThumbnailImageData()
return "小さい画像:\(thumbnailImageData)、大きな画像:\(largeImagaData)"
} catch {
throw FetchImageDataError.combinedFetchError
}
}
Task{
do {
let imageData = try await fetchImageData() //実行する
print(imageData)
} catch FetchImageDataError.largeImageFetchFailed {
print("大きな画像取得に失敗")
} catch FetchImageDataError.thumbnailFetchFailed {
print("小さな画像取得に失敗")
} catch FetchImageDataError.combinedFetchError {
print("画像データの取得に失敗")
} catch {
print("予期せぬエラー")
}
}
実行結果
小さい画像:thumbnailImage、大きな画像:largeImage
completionクロージャとasync/awaitの違いって何?
- completionクロージャを使用すると、新しく非同期関数を呼び出す際にどうしても、ネストが深くなりがちになりますが、async/awaitを使用することで、同期的な書き方が可能となります。同期的の方が見やすいですよね。
- completionの呼び忘れがなくなる。completionクロージャは言ってしまうと、クロージャなので、呼ばなくてもコンパイルエラーになりません。なので、呼び忘れが発生してしまう恐れを含んでいます。反対に、async/awaitでは値を返すことができるので、呼び忘れがなくなるのでありがたいですね。自動チェックのありがたさですね。
自分の期待した通りに動かなかったおもろい例
今回非同期処理を勉強する上で参考にさせてもらったコードを少しいじり、どのような振る舞いをするのか確かめました。
以下を試すと、自分は非同期なんだから、(1,2,3,4,5)とデバックに出力されると思っていました。しかし、結果は(1,2,5,3,4)。これは、forEachで取り出し、一般的なクロージャとして実行しているにすぎないからです。なので、最初に追加されたクロージャの実行が完了するまで、次のクロージャの処理は開始されません。
var queue = [() -> Void]()
func enqueue(operation: @escaping () -> Void) {
queue.append(operation)
}
enqueue {
print("2")
sleep(3) // 3秒待機
print("5")
}
print("1")
enqueue {
print("3")
sleep(1) //1秒待機
print("4")
}
queue.forEach{ $0() }
では、どこを変えると、自分の理想とする振る舞いをしてくれるのか?
答えはここです。
queue.forEach { closure in
DispatchQueue.global().async {
closure()
}
}
引数として受けったクロージャを実行する場面で、GCDを使って非同期処理にします。こうすることで、待機している間に、他の処理を実行し、理想の振る舞い(1,2,3,4,5)にしてくれます。
まとめ
async/awaitの方がバグの少ない処理をかけると感じました。ただ、勉強するために拝見するサンプルコードには、compltionクロージャが使われていることがあるため、compltionクロージャの知識があると、より自分の力をあげるために必要だと感じました。まだまだ、わからないことが多いので勉強していこうと思います。
参考書籍