Gunosy Tech Blog

Gunosy Tech Blogは株式会社Gunosyのエンジニアが知見を共有する技術ブログです。

Swift Concurrencyの気になるところ

こんにちは。iOS開発担当の洪です。

この記事はGunosy Advent Calendar 2021の22日目の記事です。 昨日の記事は茂木さんの2021 年の SRE チームの活動についてでした。大変面白い記事ですので是非読んでみてください。

はじめに

2021年6月のWWDCでSwift Concurrencyの発表がありました。

WOW

その後、そのSwift ConcurrencyはiOS15以上のみで利用可能と判明されました。

WOW

そして2021年12月、Xcode13.2からiOS13以上でSwift Concurrencyが利用可能になりました。

WOW

ここではSwift Concurrencyでちょっとだけコードを書いてみて感じた気になるところをまとめてみました。 (開発環境はXcode13.2.1)

Swift Concurrencyは簡単ではない

Swift ConcurrencyはJavaScriptのasync/awaitのようなPromiseのシンタックスシュガーではありません。以下の色んな新しい機能の集合体で、ちゃんと使うにはGrand Central Dispatch(GCD)以上の学習が必要です。

  • async/awaitによる一時停止と再開が可能な順次実行

Asynchronous code can be suspended and resumed later, although only one piece of the program executes at a time.
https://docs.swift.org/swift-book/LanguageGuide/Concurrency.html

  • Taskによる並列処理

Parallel code means multiple pieces of code run simultaneously—for example, a computer with a four-core processor can run four pieces of code at the same time, with each core carrying out one of the tasks.
https://docs.swift.org/swift-book/LanguageGuide/Concurrency.html

  • Actorによる実行スレッド(キュー)の管理

  • TaskGroupなどによる処理構造化

Swift Concurrencyはただではない

WWDCのSwift concurrency: Behind the scenesで説明されてるようにConcurrencyの利用にはそれなりのコストが発生します。

f:id:hongmhoon:20211220110055p:plain

そしてアプリのサイズも若干ですが増加します。(多分iOS14以下のみ?)

f:id:hongmhoon:20211220110037p:plain

最後にまだリリースされたばかりでどんなバグがあるかも不明です。

Swift5.5のSwift Concurrencyはまだ完成版ではない

Concurrency in Swift 5 and 6によると今後リリース予定のSwift6ではSendableのチェック強化なので、もっと安全なConcurrencyを目指していてSwift5.5からの移行にはそれなりの対応が必要になりそうです。 Swift5.5でも以下のフラグ設定で確認できるのでSwift Concurrencyの導入後、一度確認してみたいと思います。

-Xfrontend -warn-concurrency
-Xfrontend -enable-actor-data-race-checks
(in Xcode: Other Swift Flags)
https://twitter.com/olebegemann/status/1421144304127463427

iOSフレームワークの関連メソッドはまだiOS15以上のまま

SwiftとしてはiOS13以上でSwift Concurrencyが利用可能になりましたが、iOSの各フレームワークの関連メソッドはまだiOS15以上のままです。

f:id:hongmhoon:20211220182727p:plain

Making async system APIs backward compatibleなどでの説明のように自分でコツコツとラッパを作成するか、既存コードがある場合は次に説明するリファクタリングツールで変換しながら対応する必要があります。

One more thing

Xcodeのリファクタリング機能にConvert Function to Async, Add Async Alternative, Add Async Wrapperが追加されているので、既存コードのSwift Concurrencyへの移行時に役に立ちそうです。

f:id:hongmhoon:20211220110043p:plain

せっかくなので以下のサンプルコードで実行してみました。

func fetchData(url: URL, completionHandler: @escaping (Result<Data, Error>) -> Void) {
    URLSession.shared.dataTask(with: url) { data, _, error in
        if let error = error {
            completionHandler(.failure(error))
        } else {
            completionHandler(.success(data!))
        }
    }.resume()
}
  • Convert Function to Asyncの実行後のコード
func fetchData(url: URL) async throws -> Data {
    return try await withCheckedThrowingContinuation { continuation in
        URLSession.shared.dataTask(with: url) { data, _, error in
            if let error = error {
                continuation.resume(with: .failure(error))
            } else {
                continuation.resume(with: .success(data!))
            }
        }.resume()
    }
}
  • Add Async Alternativeの実行後のコード
@available(*, renamed: "fetchData(url:)")
func fetchData(url: URL, completionHandler: @escaping (Result<Data, Error>) -> Void) {
    Task {
        do {
            let result = try await fetchData(url: url)
            completionHandler(.success(result))
        } catch {
            completionHandler(.failure(error))
        }
    }
}


func fetchData(url: URL) async throws -> Data {
    return try await withCheckedThrowingContinuation { continuation in
        URLSession.shared.dataTask(with: url) { data, _, error in
            if let error = error {
                continuation.resume(with: .failure(error))
            } else {
                continuation.resume(with: .success(data!))
            }
        }.resume()
    }
}
  • Add Async Wrapperの実行後のコード
@available(*, renamed: "fetchData(url:)")
func fetchData(url: URL, completionHandler: @escaping (Result<Data, Error>) -> Void) {
    URLSession.shared.dataTask(with: url) { data, _, error in
        if let error = error {
            completionHandler(.failure(error))
        } else {
            completionHandler(.success(data!))
        }
    }.resume()
}

func fetchData(url: URL) async throws -> Data {
    return try await withCheckedThrowingContinuation { continuation in
        fetchData(url: url) { result in
            continuation.resume(with: result)
        }
    }
}

おわりに

色々書きましたが、Swift Concurrencyは素晴らしい技術で、これからのiOS開発を支える柱になると思っています。 みんなで全集中常中でConcurrencyの呼吸が使えるように鍛錬していきましょう!

それでは明日の記事もお楽しみに!