Upgrade to Pro — share decks privately, control downloads, hide ads and more …

タイミーiOSアプリへの Swift Concurrency 導入までの軌跡

tanako
September 13, 2023

タイミーiOSアプリへの Swift Concurrency 導入までの軌跡

同僚と一緒に導入を行ったSwift Concurrencyについて

tanako

September 13, 2023
Tweet

More Decks by tanako

Other Decks in Programming

Transcript

  1. 田中 幸一(@_tanakoo) • 株式会社タイミー • プロダクト本部 • ワーキングリレーションチーム • iOSエンジニア

    イチオシのiOSDCのトーク 「複雑さに立ち向かうためのコードリーディ ング入門」by shiz 自己紹介
  2. 目次 • タイミーについて • Swift Concurrencyのおさらい • Swift Concurrencyの導入理由 •

    開発とリリースの方針 • 起きた問題と対策 • まとめ
  3. 5

  4. 6

  5. タイミーの実績 スキマ バイト No.1 ※2022年8月時点 ※1 [調査方法]デスクリサーチ及びヒアリング調査 [調査期間]2021年2月8日~22日 [調査概要]スキマバイトアプリ サービスの実態調査 [調査対象]2020年12月までにサービスを開始しているスキマバイトアプリ10サービス

    [調査実施]株式会社ショッ パーズアイ ※2 [出典]AppStoreライフスタイルカテゴリーランキング(2021年5月時点) 8 累計求人案件数 ・ダウンロード数 ※1 ※2 導入事業者数 46,000企業 ワーカー数 500万人
  6. 従来の非同期処理の例 func downloadData(from url: URL, completion: @escaping (Result<Data, Error>) ->

    Void) { URLSession.shared.dataTask(with: url) { data, response, error in if let error = error { completion(.failure(error)) return } if let response = response as? HTTPURLResponse { guard response.statusCode == 200 else { completion(.failure(ResponseError(statusCode: response.statusCode))) return } } completion(.success(data!)) }.resume() }
  7. 従来の非同期処理の例 func downloadData(from url: URL, completion: @escaping (Result<Data, Error>) ->

    Void) { URLSession.shared.dataTask(with: url) { data, response, error in if let error = error { completion(.failure(error)) return } if let response = response as? HTTPURLResponse { guard response.statusCode == 200 else { completion(.failure(ResponseError(statusCode: response.statusCode))) return } } completion(.success(data!)) }.resume() } 処理のたびにネストが増える 完了処理を呼び忘れる可能性がある
  8. async/awaitで書き直すと func downloadData(from url: URL) async throws -> Data {

    let (data, response) = try await URLSession.shared.data(from: url) if let response = response as? HTTPURLResponse { guard response.statusCode == 200 else { throw ResponseError(statusCode: response.statusCode) } } return data }
  9. async/awaitで書き直すと func downloadData(from url: URL) async throws -> Data {

    let (data, response) = try await URLSession.shared.data(from: url) if let response = response as? HTTPURLResponse { guard response.statusCode == 200 else { throw ResponseError(statusCode: response.statusCode) } } return data } asyncキーワードで非同期関数であること を明示
  10. async/awaitで書き直すと func downloadData(from url: URL) async throws -> Data {

    let (data, response) = try await URLSession.shared.data(from: url) if let response = response as? HTTPURLResponse { guard response.statusCode == 200 else { throw ResponseError(statusCode: response.statusCode) } } return data } エラーを返す場合 throwsを付与 成功結果は単純に返り値になる
  11. async/awaitで書き直すと func downloadData(from url: URL) async throws -> Data {

    let (data, response) = try await URLSession.shared.data(from: url) if let response = response as? HTTPURLResponse { guard response.statusCode == 200 else { throw ResponseError(statusCode: response.statusCode) } } return data } potential suspension pointsであり、 関数が停止する可能性がある。 停止した場合、スレッドは開放され、 別の仕事を行うことができる。
  12. データ競合の例 final class Counter { private var count: Int =

    0 func increment() -> Int { count += 1 return count } }
  13. データ競合の例 let counter: Counter = .init() DispatchQueue.global().async { print(counter.increment()) }

    DispatchQueue.global().async { print(counter.increment()) } 1,2 、 2,1 の結果を期待。 しかし、両方の実行結果が2になりうる
  14. データ競合の例 final class Counter { private var count: Int =

    0 func increment() -> Int { count += 1 return count } } 1回目の処理でreturnする直前に、 2回目の処理が呼ばれることで両方 2となる
  15. 従来の排他制御の例 final class Counter { private let queue: DispatchQueue =

    .init(label: "Counter") private var count: Int = 0 func increment(completion: @escaping (Int) -> Void) { queue.async { [self] in count += 1 completion(count) } } }
  16. 従来の排他制御の例 final class Counter { private let queue: DispatchQueue =

    .init(label: "Counter") private var count: Int = 0 func increment(completion: @escaping (Int) -> Void) { queue.async { [self] in count += 1 completion(count) } } } クリティカルセクションを明示的に専用の実 行キューで守る必要があった。
  17. Actorで書き直すと actor Counter { private var count: Int = 0

    func increment() -> Int { count += 1 return count } }
  18. Actorで書き直すと let counter: Counter = .init() Task.detached { print(await counter.increment())

    } Task.detached { print(await counter.increment()) } 1,2、あるいは2,1と必ず出力される
  19. • 23.07.11 フィーチャーフラグをOFF • 23.08.01 一部の通信だけフラグをON • 23.08.07 全ての通信のフラグをON •

    23.08.15 フィーチャーフラグを削除 ※ 現行アプリは日付でバージョニングしています。 リリースされたバージョンの具体
  20. 対策 private var refreshTask: Task<Token?, Never>? func token() async ->

    Token? { if let refreshTask { return await refreshTask.value } ... } トークンのread時も実行中のタスクの結果を使うように変更した。
  21. 原因 func request<T>(_ request: T) -> Single<T.Response> where T: RequestDefinition

    { return Single.create { single in let task = Task { do { let result = try await self._requestAsync(request) single(.success(result)) } catch { single(.failure(error)) } } return Disposables.create { task.cancel() } } }
  22. 原因 func request<T>(_ request: T) -> Single<T.Response> where T: RequestDefinition

    { return Single.create { single in let task = Task { do { let result = try await self._requestAsync(request) single(.success(result)) } catch { single(.failure(error)) } } return Disposables.create { task.cancel() } } } ここまではメインスレッドだったが ..
  23. 原因 func request<T>(_ request: T) -> Single<T.Response> where T: RequestDefinition

    { return Single.create { single in let task = Task { do { let result = try await self._requestAsync(request) single(.success(result)) } catch { single(.failure(error)) } } return Disposables.create { task.cancel() } } } Taskのclosure内の先頭でワーカースレッドに なっていた。(Actor Contextの引き継ぎは静的 にコンパイル時に解決されるため、明示的な @MainActorなどの付与がないと引き継がれな い)
  24. 残っている課題 • Swift Concurrencyという新たな複雑性が導入された ◦ チームでの学習が引き続き必要 • Domainモジュール側にRxが残っている • アプリケーション全体にSwift

    Concurrencyを適用する ◦ 通信の表層ではまだRxSwiftのSingleを返している • Strict Concurrency Checkingの有効化 • まだまだリファクタの余地がある