🗂

【翻訳】Why use swift async-await?

2023/07/19に公開

Swift の開発では、クロージャや完了ハンドラを使った非同期プログラミングが多いですが、これらの API は使いにくいです。 Swift 5.5 から、 Xcode 13 は async/await を使って構造化された方法で非同期と並列コードを書くための組み込みサポートを導入しました。

async-await

async-await を使うことで、値を返すために完了ハンドラを使わずに非同期コードを書くことができます。構造化された同時実行は、 Swift の複雑な非同期コードの可読性を向上させます。

async:

関数/プロパティが非同期であることを示します。さらに、非同期関数/プロパティが値または結果を返すまで、コードの実行を一時停止することができます。

await:

非同期関数/プロパティが値を返すのを待つ間、コードの実行が停止する可能性があることを示します。

以下は、 async/await を使って関数やプロパティを非同期にする手順です。

  1. 定義内の関数/プロパティ名の最後に async キーワードを使用して関数/プロパティをマークします。

  2. 非同期関数/プロパティがエラーを発生させる場合は、async キーワードの後に続く throw キーワードでマークします。

  3. 関数/プロパティは成功値を返します。呼び出し側がエラーを投げた場合、呼び出し側の do-catch ブロックで処理されます。

  4. 同期コードから async 関数/プロパティを直接呼び出すことはできませんので、バックグラウンド・スレッドで並列実行される Task にラップする必要があります。

完了ハンドラーは構造化されていないのに対し、 async-await は構造的なシーケンシャル・パターンに従っています。以下のコード・スニペットは、クロージャの非構造化パターンと非同期/待ち受けの構造化パターンをそれぞれ示しています。

async-await 関数は構造化された同時実行を可能にし、それゆえ Swift の複雑な非同期コードの可読性を向上させます。以下のコードは async-await を使用しています。

func fetchThumbnail(for url: URL) async throws -> UIImage { // 1. Call the method
        // 2. Fetch images from url and return image
        let ( data, response) = try await URLSession.shared.data(from: url)
        // 3. Throw error if response status code is not succeess
        guard (response as? HTTPURLResponse)?.statusCode == 200 else { throw ARError.badID}
        // 4. Create Image from response data
        let image = UIImage(data: data)
        // 5. Resize image and return
        // 6. Throw error if not valid image
        guard let thumbnail = await image?.byPreparingThumbnail(ofSize: CGSize(width: 300, height: 300)) else { throw ARError.badImage }
        // 7. return valid image
        return thumbnail
    }

完了ハンドラーを使って画像を取得する前に、呼び出しメソッドが戻ってくるのが分かるでしょう。結果を受け取ると、完了コールバックに戻ります。このような非構造的な実行順序をたどるのは難しいです。もし、補完コールバックの中で別の非同期メソッドを実行すると、別のクロージャコールバックが追加されることになります。クロージャが追加されるたびにインデントがもう一段階追加され、実行順序を追うのが難しくなります。

func fetchThumbnail(for url: URL, completion: @escaping(UIImage?, Error?) -> Void) {
        // 1. Call the method
        let task = URLSession.shared.dataTask(with:url) { data, response, error in // 4. Asynchronous method returns
            
            if let error = error {
                // 5. Any error comes, completion block return with error
                completion(nil, error)
            } else if (response as? HTTPURLResponse)?.statusCode != 200 {
                // 6. Response status code is not succeess, completion block return with error
                completion(nil, ARError.badID)
            } else {
                guard let image = UIImage(data: data!) else {
                    // 7. Not a valid image, completion block return with error
                    completion(nil, ARError.badImage)
                    return
                }
                image.prepareThumbnail(of: CGSize(width: 300, height: 300)) { thumbnail in
                    guard let thumbnail = thumbnail else {
                        // 8. Not a valid image after resize, completion block return with error
                        completion(nil, ARError.badImage)
                        return
                    }
                    // 9. valid image, completion block return with image object
                    completion(thumbnail, nil)
                }
            }
        }
        // 2. Resume data task
        task.resume()
    }
    // 3.Calling method exits

Taskとは?

すべての非同期関数は何らかのタスクの一部として実行されます。関数が非同期呼び出しを行うと、呼び出された関数は同じタスクの一部として実行されます(呼び出し側は関数が戻るのを待ちます)。同様に、関数が非同期呼び出しから戻ると、呼び出し元は同じタスクで実行を再開します。非同期コードは、同期メソッドから直接呼び出すことはできません。同期メソッドから非同期コードを呼び出すには、タスクを作成し、そこから非同期関数を呼び出す必要があります。タスクは同期コードと非同期コードの橋渡しをします。タスクは、非同期作業を別のスレッドで実行・管理できる環境を作ります。タスク・タイプを通じて、非同期コードの実行、一時停止、キャンセルができます。

Task {
    let resultImage = try await fetchThumbnail(for: imageURL)
    ... // futher work
    ... // take over (asynchronously)
}

タスクを作成する間、タスクがどれほど緊急であるかに基づいて、優先度も指定することが重要です。タスクが作成されるときに、優先順位を直接割り当てることができますが、そうしない場合、Swiftは自動的に優先順位を決定するための3つのルールに従います。

  1. タスクが他のタスクから作成された場合、子タスクは親タスクの優先度を継承します。

  2. 新しいタスクがタスクとは対照的にメインスレッドから直接作成された場合、自動的にuserInitiatedの最高の優先度が割り当てられます。

  3. 新しいタスクが別のタスクやメインスレッドによって作られなかった場合、Swift はスレッドの優先順位を問い合わせようとします。

最も低い優先順位は、ユーザーから見てそれほど緊急でない処理を実行できるバックグラウンドです。最も高い優先度はhighで、これはuserInitiatedと同義であり、ユーザーにとって重要なタスクであることを示します。デフォルトの優先度は中であり、他の操作と同じように扱われます。

複数の非同期関数の並列実行

無関係な複数の非同期関数を並列に実行したい場合、それらをそれぞれのタスクにまとめ、並列に実行することができます。実行順序は不定です。

Task(priority: .medium) {
	let result1: UIImage = try await fetchThumbnail(for: profileURL)
}

Task(priority: .medium) {
	let result2: UIImage = try await fetchThumbnail(for: profileThumbnailURL)
}

Task(priority: .medium) {
	let result3: UIImage = try await fetchThumbnail(for: productImageURL)
}

複数の非同期処理を並行して実行し、結果を一度に得る(並行バインディング)

複数の非同期処理を並行して実行し、結果を一度に得ることができます。
async let concurrent bindingとも呼ばれます。

Task(priority: .medium) {
    do {

        async let image1 = try await fetchThumbnail(for: productImageURL1)
        
        async let image2 = try await fetchThumbnail(for: productImageURL2)
        
        async let image3 = try await fetchThumbnail(for: productImageURL3)
        
        let imagesArray = try await [image1, image2, image3]
        
    } catch {
        // Handle Error
    }
}

非同期シーケンス

Swift 5.5 の新しい同時実行システムは、非同期シーケンスとイテレータの概念を導入しています。AsyncSequence は Sequence 型に似ていて、一度に一つずつステップスルーできる値のリストを提供し、非同期性を追加します。 AsyncSequence は、最初に使用するときに、その値のすべて、一部、または全く利用できない場合があります。その代わりに、 await を使用して、値が利用可能になったときに値を受け取ります。

for await i in Counter(howHigh: 10) {   
  print(i, terminator: " ")
}

AsyncSequence は Sequence の非同期バリアントであり、 AsyncSequence は単なるプロトコルです。AsyncSequence プロトコルは AsyncIterator を提供し、値の開発と潜在的な格納を行います。非同期のイテレーションを作成するには、 AsyncSequence プロトコルに準拠し、 makeAsyncIterator メソッドを実装する必要があります。

struct ImageURL: AsyncSequence {
    typealias Element = URL
    var urlStr: [String]

    func makeAsyncIterator() -> ImageURLData {
        ImageURLData(urls: urlStr)
    }
}

struct ImageURLData: AsyncIteratorProtocol {
    var urls: [String]
    fileprivate var index = 0

    mutating func next() async throws -> URL? {
        guard index < urls.count else {
                    return nil
        }
        let filePath = urls[index]
        index += 1

        if let strURL = URL(string: "https://image.tmdb.org/t/p/w500/\(filePath)") {
            return strURL
        }
        return nil
    }
}

private func getImages() {
    let imagesFilePath = ["cOF0InT1qQVUeNjqxjF7gtEtL5L.jpg", "hT3OqvzMqCQuJsUjZnQwA5NuxgK.jpg", "8DwrHSpilQiZiegR9T8Q69ey8ru.jpg" ]
    
    Task {
        var images: [UIImage] = [UIImage]()
        for try await filePath in ImageURL(urlStr: imagesFilePath) {
            do {
                async let (image_1) =   UIImage().fetchThumbnail(for: filePath)
                try await images.append(image_1)
            } catch {
                print(error)
            }
        }
    }

非同期プロパティ

プロパティを非同期にするには、プロパティのゲッターの後に async を追加する必要があります。

extension UIImage { 
   // async property 
   var thumbnail: UIImage? {
     get async { 
         let size = CGSize(width: 300, height: 300) 
         return await self.byPreparingThumbnail(ofSize: size)
       }
   }
}

読み取り専用プロパティだけが非同期にできるため、そのプロパティ用に明示的にセッターを作成する必要があります。非同期プロパティにセッターを提供しようとすると、コンパイラはエラーを発生させます。

throws を持つ非同期プロパティ

非同期プロパティは throws キーワードにも対応しています。プロパティの定義で async キーワードの後にthrows キーワードを追加し、エラーを投げるメソッドで try await を使用する必要があります。

extension UIImage { 
   var posterImage: UIImage {
      get async throws {
     do {
        let ( data, _) = try await URLSession.shared.data(from: imageUrl)
         return UIImage(data: data)!
       } catch {
           throw error
         }
      }
   }
}

非同期コンテキスト内での defer の使用

deferブロックはコンテキストを抜ける前の最後に実行され、リソースのクリーンアップが見落とされないように実行が保証されます。

private func getMovies() {
  defer {
    print("Defer statement outside async")
  }
  Task {
     defer { 
        print("Defer statement inside async")
    }
    let result = try? await     ARMoviesViewModel().callMoviesAPIAsyncAwait(ARMovieResponse.self)
    switch result {
       case .failure(let error): print(error.localizedDescription)
       case .success(let items): movies = items.results
       case .none: print("None")
    }
   print("Inside the Task")
  }
 print("After the Task")
}
// After the Task
// Defer statement outside async
// Inside the Task
// Defer statement inside async

非同期APIのcontinuation

古いコードでは、サードパーティライブラリや独自の関数で非同期関数を使用する必要がある場合に、完了ハンドラを使用して作業が完了したことを通知しています。continuation の助けを借りて、完了ハンドラを非同期API にラップすることができます。

continuation にはいくつかの種類があります。

・ withCheckedThrowingContinuation

・ withCheckedContinuation

・ withUnsafeThrowingContinuation

・ withUnsafeContinuation

func getMoviesPostersAPI(_ movie: ARMovie, completion: @escaping PostersCompletionClosure) {
        let reviewURL = checkURL(ARAPI.moviePosters, movie)
        guard let url = reviewURL else {
            completion(nil, ARNetworkError.invalidUrl)
            return
        }
        ARNetworkManager().executeRequest(url: url, completion: completion)
    }
    
    func allPosters(_ movie: ARMovie) async -> (ARMoviePoster?, Error?) {
        await withCheckedContinuation { continuation in
            getMoviesPostersAPI(movie) { posters, error in
                continuation.resume(returning: (posters, error))
            }
        }
    }

continuation は正確に1度だけ再開されなければなりません。0回ではなく、2回以上でもありません。

async-await の利点

・ ネストされたクロージャによる破滅のピラミッド問題の回避

・ コードの削減

・ 可読性の向上

・ 完了ブロックは呼ばれるかもしれないし、呼ばれないかもしれませんが、async/await の安全性と結果は保証されます。

デモ・プロジェクトはGithubでチェックできます。
https://github.com/ashokrwt31/AsyncAwaitPOC

この記事では、新しい Swift Concurrency Model の中での async-await の役割と、なぜそれが私たちに関係することができるのかを認識しました。この記事が、現実の問題で使用する async-await のさまざまな側面について、あなたにいくつかの新しい洞察を与えたことを願っています。また、データ競合を防ぐための Swift Actor についての私のブログも読むことができます。

【翻訳元の記事】

Why use swift async-await?
https://medium.com/@ashokrawat086/why-to-use-swift-async-await-b19993be27cf

Discussion