👏

Swift Concurrencyについての基礎

2023/12/07に公開

Swift Concurrencyとは?

ざっくり、
非同期と並行処理を、大きくしたのこいつらでやる

Async/Await。

非同期処理を同期処理と同じような方法でより読みやすく、より正確にかけるようにした構文

Task

プログラムの一部として非同期で実行できる作業の単位。
すべての非同期コードは何らかのタスクの一部として実行されます。

Actor

非同期処理で起こりやすい、データ競合を防いでくれます。

そもそも非同期って今までどうやってたっけ?

今までは↓

コールバックによる非同期

func callbackFetchThumbnail(for id: String, completion: @escaping (UIImage?, Error?) -> Void) {
    let request = thumbnailURLRequest(for: id)
    let dataTask = URLSession.shared.dataTask(with: request) { data, response, error in
        if let error = error {
            completion(nil, error)
        } else {
            guard let image = UIImage(data: data!) else { completion(nil, Error.self as? Error); return }
        }
    }
    dataTask.resume()
}

ネストが深くなり、読みづらい。
上から下に順番に処理が行われず、コールバックは関数を抜けた後にに実行され、実際に実行されるタイミングも結果が返ってくるタイミング次第となり不明瞭

Concurrencyを使った非同期

それに対してConcurrencyでは
async/await では、同期的な処理(上から一個づつ流れていく処理)と非同期的な処理をほぼ同じ見た目でネストもなくシンプルに書け、処理も上から順番に行われます。

func asyncFetchThumbnail(for id: String) async throws -> UIImage {
    let request = thumbnailURLRequest(for: id)
    let (data, response) = try await URLSession.shared.data(for: request)
    validateResponse(response)
    guard let image = await UIImage(data: data)?.byPreparingThumbnail(ofSize: CGSize()) else {
        throw ThumbnailFailedError()
    }
    return image
}

変更点 された点は主に3つで

await

このasyncがついてることによって、この関数は内部で処理がサスペンドしてスレッドが変わる可能性がありますよ。ってことをシステムに伝える。
サスペンドってい言うのは、現在の処理を中断して実行していたスレッドで他の処理を実行可能にすること。

例えばこの関数がviewDidLoadの中だとして、この関数の下に、別のコードがあれば、サスペンドしたタイミングで、元々この関数を実行していたスレッドでその別のコードが走るよ

throws

throwsがあることによって、errorをthrowしたり、catchしたりもできる。
errorを返す必要がない時は、throwsをつけなくても良い

await

実際に処理がサスペンドするかもしれないタイミングはここのawaitがあるところで、中断されます。そして、awaitの右側の処理が行われ、結果が返ってきたら変数に代入され、処理が再開される。
awaitはasyncの中でのみ使えます。

既存のコールバックベースの非同期APIを変換する

func fetchMyData() async throws -> Data {
    return try await withCheckedThrowingContinuation { continuation in
        fetchDataAsync { result in
            switch result {
            case .success(let data):
                continuation.resume(returning: data)
            case .failure(let error):
                continuation.resume(throwing: error)
            }
        }
    }
}

withCheckedThrowingContinuationってのが使われます。

これは非同期的にDataの取得を行い、クロージャで結果を受け取れることがわかります。このクロージャの中で結果が取得できていれば continuation オブジェクトに対してresume(returning:) メソッドを呼び、エラーが発生していればresume(throwing:) メソッドを呼んでいます。これにより、既存のクロージャで結果を受け取るインターフェースを簡単に async/await に対応させることができます。

また、並列に非同期関数があるとき

func fetchOneThumbnail(withID id: String) async throws -> UIImage {
    let imageReq = imageRequest(for: id), metadataReq = metadataRequest(for: id)
   // let (data, _) = try await URLSession.shared.data(for: imageReq)
 // let (metadata, _) = try await URLSession.shared.data(for: metadataReq)
    async let (data, _) = URLSession.shared.data(for: imageReq)
    async let (metadata, _) = URLSession.shared.data(for: metadataReq)
    guard let size = parseSize(from: try await metadata),
          let image = try await UIImage(data: data)?.byPreparingThumbnail(ofSize: size)
    else {
        throw ThumbnailFailedError()
    }
    return image
}

URLSessionの前にtry awaitをつけるのではなく、async letを使う

上のコードだと、try awaitのタイミングで2回も処理を待つ必要があるが、参照されたところにtry awaitをつけることにより、並行処理ができる。

仕組みとしては、async letにきた時点で、右側の処理の子タスクが作られ、仮の値が左側に渡される。
右の処理が終わったら、参照元に値が送られる。

同じ型を返す動的な数の子タスクの実行時

func fetchThumbnails444(for ids: [String]) async throws -> [String: UIImage] {
    var thumbnails: [String: UIImage] = [:]
    try await withThrowingTaskGroup(of: (String, UIImage).self) { group in
        for id in ids {
            group.addTask {
                return (id, try await self.fetchOneThumbnail(withID: id))
            }
        }
        for try await (id, thumbnail) in group {
            thumbnails[id] = thumbnail
        }
    }
    return thumbnails
}

withTaskGroup withThrowingTaskGroupプロパティ

group.addTaskで子タスクを追加すると処理を開始し、for文でid数分だけ子タスクを生成して同時並行に処理を実行する。
追加されたそれぞれのタスクが結果を返すたびに値を返し for await ... in 文で結果を取得することができます。

UIkitなどの同期的なコードから直接呼びたい時

Task.init(priority:operation)

func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
    Task {
        let ids = getThumnailIDs(for: indexPath.row)
        let thumbnails = await fetchThumbnails(for: ids)
    }
}

基本これつけて始まる。
周囲のコンテキストを引き継ぐ。

Task.detached(priority:operation)

func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
    Task {
        let ids = getThumnailIDs(for: indexPath.row)
        let thumbnails = await fetchThumbnails(for: ids)
        
        Task.detached(priority: .background) {
            writeLocalCache(thumbnails)
        }
    }
}

周囲のコンテキストから独立したタスクを生成。
メインスレッド上のTaskの中とかで、バックグランドでやって欲しい処理がある時とかに使う。

データ競合を防ぐActor

actor Counter {
    var value = 0

    func increment() -> Int {
        value = value + 1
        return value
    }
}

actorはstruct や class と並んで新たに Swift に導入された型の一種。
アクターは既存の型と同様に、プロパティやメソッド、イニシャライザなどを持つことが可能。
異なるのは、アクターはそのインスタンスが持つデータをプログラムの他の部分から "隔離する"("isolate" する)という点。
アクターの外から内部にアクセスするにはawaitをつける必要があり、これだけでアクターに変換できデータ競合を防ぐ。

MainActor

private func fetchUnOpenedNotifications() async {
    do {
        let data = try await interactor.fetchUnreadNotifications()
    } catch {
        Task { @MainActor in
            view?.errorAlert(error)
        }
    }
}

これは処理がMainThread上で行われることが保証されている
こんな感じで、非同期関数の中でerrorの際に、view側にエラーアラートを出すときとかに使える。

@MainActor
protocol MyPagePresentation: AnyObject {

特定のクラス、構造体、列挙型、またはそれらのメソッドやプロパティがメインスレッド(メインアクター)で実行されることを保証するために使用される。これは主にUIの更新やメインスレッドでのみ安全に実行できる操作に重要。

Arsaga Developers Blog

Discussion