Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
タイミーiOSアプリへの Swift Concurrency 導入までの軌跡
Search
tanako
September 13, 2023
Programming
4
1.6k
タイミーiOSアプリへの Swift Concurrency 導入までの軌跡
同僚と一緒に導入を行ったSwift Concurrencyについて
tanako
September 13, 2023
Tweet
Share
More Decks by tanako
See All by tanako
SwiftPM Integration into Xcode
_tanako
0
140
roppongiswift6.pdf
_tanako
1
350
iOSエンジニアの為のgrpc-swift入門
_tanako
6
4.6k
grpc-swiftの紹介
_tanako
0
1.7k
How to write basic unittests
_tanako
0
180
Other Decks in Programming
See All in Programming
Jakarta Concurrencyによる並行処理プログラミングの始め方 (JJUG CCC 2024 Fall)
tnagao7
1
290
AWS Lambdaから始まった Serverlessの「熱」とキャリアパス / It started with AWS Lambda Serverless “fever” and career path
seike460
PRO
1
260
エンジニアとして関わる要件と仕様(公開用)
murabayashi
0
280
Hotwire or React? ~アフタートーク・本編に含めなかった話~ / Hotwire or React? after talk
harunatsujita
1
120
【Kaigi on Rails 2024】YOUTRUST スポンサーLT
krpk1900
1
330
Webの技術スタックで マルチプラットフォームアプリ開発を可能にするElixirDesktopの紹介
thehaigo
2
1k
シールドクラスをはじめよう / Getting Started with Sealed Classes
mackey0225
4
640
Amazon Bedrock Agentsを用いてアプリ開発してみた!
har1101
0
330
弊社の「意識チョット低いアーキテクチャ」10選
texmeijin
5
24k
どうして僕の作ったクラスが手続き型と言われなきゃいけないんですか
akikogoto
1
120
Content Security Policy入門 セキュリティ設定と 違反レポートのはじめ方 / Introduction to Content Security Policy Getting Started with Security Configuration and Violation Reporting
uskey512
1
530
LLM生成文章の精度評価自動化とプロンプトチューニングの効率化について
layerx
PRO
2
190
Featured
See All Featured
Visualization
eitanlees
145
15k
Measuring & Analyzing Core Web Vitals
bluesmoon
4
120
Code Reviewing Like a Champion
maltzj
520
39k
Navigating Team Friction
lara
183
14k
[Rails World 2023 - Day 1 Closing Keynote] - The Magic of Rails
eileencodes
33
1.9k
Optimizing for Happiness
mojombo
376
70k
Distributed Sagas: A Protocol for Coordinating Microservices
caitiem20
329
21k
Teambox: Starting and Learning
jrom
133
8.8k
Bash Introduction
62gerente
608
210k
Fantastic passwords and where to find them - at NoRuKo
philnash
50
2.9k
The Art of Programming - Codeland 2020
erikaheidi
52
13k
The Art of Delivering Value - GDevCon NA Keynote
reverentgeek
8
860
Transcript
2023/09/13 田中幸一 タイミーiOSアプリへの Swift Concurrency導入までの軌跡 @_tanakoo
田中 幸一(@_tanakoo) • 株式会社タイミー • プロダクト本部 • ワーキングリレーションチーム • iOSエンジニア
イチオシのiOSDCのトーク 「複雑さに立ち向かうためのコードリーディ ング入門」by shiz 自己紹介
目次 • タイミーについて • Swift Concurrencyのおさらい • Swift Concurrencyの導入理由 •
開発とリリースの方針 • 起きた問題と対策 • まとめ
1 タイミーについて
5
6
募集人数の推移 7 ※1:2022年4Qと2021年4Qの比較 コロナ禍においても、 過去に例を見ない程の 加速的高成長を実現。
タイミーの実績 スキマ バイト No.1 ※2022年8月時点 ※1 [調査方法]デスクリサーチ及びヒアリング調査 [調査期間]2021年2月8日~22日 [調査概要]スキマバイトアプリ サービスの実態調査 [調査対象]2020年12月までにサービスを開始しているスキマバイトアプリ10サービス
[調査実施]株式会社ショッ パーズアイ ※2 [出典]AppStoreライフスタイルカテゴリーランキング(2021年5月時点) 8 累計求人案件数 ・ダウンロード数 ※1 ※2 導入事業者数 46,000企業 ワーカー数 500万人
2 Swift Concurrencyの おさらい
Swift Concurrencyとは Swift 5.5から導入された非同期処理や並行処理を安全かつ簡潔に書ける仕組み • async/await • Actor • Structured
Concurrency • etc.. ここではasync/awaitとActorだけを簡単におさらい
async/awaitとは • 非同期処理を同期処理と同じ見た目で簡潔に書ける仕組み • これによりコールバック地獄を回避できたり様々な利点がある
従来の非同期処理の例 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() }
従来の非同期処理の例 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() } 処理のたびにネストが増える 完了処理を呼び忘れる可能性がある
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/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キーワードで非同期関数であること を明示
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を付与 成功結果は単純に返り値になる
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であり、 関数が停止する可能性がある。 停止した場合、スレッドは開放され、 別の仕事を行うことができる。
Actorとは • 並行処理におけるデータ競合やデッドロックを防ぐ仕組み • Actorはキューのようなものを持っていて処理を順番に実行する • これにより処理が同時に1つしか実行されないことを保証(Actor Isolation)
データ競合の例 final class Counter { private var count: Int =
0 func increment() -> Int { count += 1 return count } }
データ競合の例 let counter: Counter = .init() DispatchQueue.global().async { print(counter.increment()) }
DispatchQueue.global().async { print(counter.increment()) } 1,2 、 2,1 の結果を期待。 しかし、両方の実行結果が2になりうる
データ競合の例 final class Counter { private var count: Int =
0 func increment() -> Int { count += 1 return count } } 1回目の処理でreturnする直前に、 2回目の処理が呼ばれることで両方 2となる
従来の排他制御の例 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) } } }
従来の排他制御の例 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) } } } クリティカルセクションを明示的に専用の実 行キューで守る必要があった。
Actorで書き直すと actor Counter { private var count: Int = 0
func increment() -> Int { count += 1 return count } }
Actorで書き直すと let counter: Counter = .init() Task.detached { print(await counter.increment())
} Task.detached { print(await counter.increment()) } 1,2、あるいは2,1と必ず出力される
3 Swift Concurrencyの 導入理由
Swift Concurrencyの導入理由 • トークンのリフレッシュ処理がRxSwiftで実装されており複雑化していた • トークン取得の排他制御に漏れがありエッジケースで問題が起きていた • そもそも標準APIで良いところはRxSwift剥がしていきたい
4 開発とリリースの方針
開発の方針 • 初手のスコープを小さく保つ ◦ 外部ライブラリにAPIリクエストを投げる部分 ◦ トークンリフレッシュ部分 • 新しい技術をペアで開発して知見を広げる ◦
同僚が前半、後半を自分が引き継いで実装
リリースの方針 • 影響を局所化する ◦ 1部の通信から徐々に全体に適用 ◦ 段階リリースの活用 ◦ bugが修正されているXcode14.3.1を使ってリリースする •
ロールバックしやすくする ◦ Remote ConfigでFeature Flagを用意
• 23.07.11 フィーチャーフラグをOFF • 23.08.01 一部の通信だけフラグをON • 23.08.07 全ての通信のフラグをON •
23.08.15 フィーチャーフラグを削除 ※ 現行アプリは日付でバージョニングしています。 リリースされたバージョンの具体
5 起きた問題と対策
トークンリフレッシュ中にトークンのreadが走った際に古いトークンを取得してしまうこと があった。 原因 swiftのActor内でawaitを呼んだ際のreentrancy(再入可能性)を考慮できていなかっ た。(writeは実行中のtaskを待機していたがreadの考慮が漏れていた) 開発中に起きた問題1 public actor AuthManager {
func token() async -> Token? { // awaitでサスペンドされるとresume前にここが呼ばれる可能性がある } func refreshToken() async -> Token? { // この処理の中で awaitがあった }
対策 private var refreshTask: Task<Token?, Never>? func token() async ->
Token? { if let refreshTask { return await refreshTask.value } ... } トークンのread時も実行中のタスクの結果を使うように変更した。
トークンのリフレッシュ中にタブを切り替えるとセッションが切れ る問題が起きていた。 原因 API通信クラスがActorを保持しており、アプリはタブ毎にAPI通 信クラスを生成している。そのため、タブの数だけActorが生ま れており、Actor1のリフレッシュ中にActor2(Actor1とは異なる インスタンス)が古い値を見ていた。 開発中に起きた問題2
Actorをシングルトンにして対処 余談 当初はGlobalActorで実装していたが、コードレビューで意図の理解しづらさなどの指摘 を受けてシンプルにシングルトン化した。(@MainActorのようにattributeも利用していな かったので今考えても使い方を間違っていたと感じています) 対策
一部の通信をSwift Concurrencyを使うようにした。 この段階では問題は起きなかった。 初回リリース
一部の画面でクラッシュが起きた。 原因 await後にView側でUIを操作するが、ワーカースレッドからの操作になっていて、スレッ ド違反でクラッシュしていた。 全体リリースの開発を始めたところ..
原因 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() } } }
原因 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() } } } ここまではメインスレッドだったが ..
原因 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などの付与がないと引き継がれな い)
callbackを @MainActor にいれることにした。 1API毎Actor hoppingが発生するがオーバーヘッドは小さいと判断して、MainActorで ラップした。 対策
しかしクラッシュが発生してしまった • クラッシュ検知15分後にはFeature FlagをOFF & 段階リリースを中止 • 同じようなrequest関数が他にも存在、MainActorでのラップが漏れていた • 最終的にクラッシュは修正、フィーチャーフラグも削除済み
全体リリース!
6 まとめ
Swift Concurrency導入で起きた良いこと • トークン管理がより安全になった • RxSwiftのコードが一部なくなった • 通信処理の見直しによりリトライの過不足がなくなった • チームに新しい技術の知見が広がった
残っている課題 • Swift Concurrencyという新たな複雑性が導入された ◦ チームでの学習が引き続き必要 • Domainモジュール側にRxが残っている • アプリケーション全体にSwift
Concurrencyを適用する ◦ 通信の表層ではまだRxSwiftのSingleを返している • Strict Concurrency Checkingの有効化 • まだまだリファクタの余地がある
まとめ • 新しい技術は小さくロールバックできるようにリリースしよう • 新しい技術はみんなでPRを作る or ペアプロで知見を共有しよう • Actorの中でawaitする場合のreentrancyな性質に注意 •
Actor Contextの引き継ぎは静的(コンパイル時)に解決される