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.8k
タイミー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
380
iOSエンジニアの為のgrpc-swift入門
_tanako
6
4.7k
grpc-swiftの紹介
_tanako
0
1.7k
How to write basic unittests
_tanako
0
190
Other Decks in Programming
See All in Programming
データの整合性を保つ非同期処理アーキテクチャパターン / Async Architecture Patterns
mokuo
53
18k
Bedrock Agentsレスポンス解析によるAgentのOps
licux
3
910
Kotlinの開発でも AIをいい感じに使いたい / Making the Most of AI in Kotlin Development
kohii00
5
960
AIの力でお手軽Chrome拡張機能作り
taiseiue
0
190
React 19アップデートのために必要なこと
uhyo
8
1.4k
責務と認知負荷を整える! 抽象レベルを意識した関心の分離
yahiru
8
1.3k
未経験でSRE、はじめました! 組織を支える役割と軌跡
curekoshimizu
1
140
Kubernetes History Inspector(KHI)を触ってみた
bells17
0
250
1年目の私に伝えたい!テストコードを怖がらなくなるためのヒント/Tips for not being afraid of test code
push_gawa
1
500
Visual StudioのGitHub Copilotでいろいろやってみる
tomokusaba
1
210
From the Wild into the Clouds - Laravel Meetup Talk
neverything
0
110
新宿駅構内を三人称視点で探索してみる
satoshi7190
2
120
Featured
See All Featured
Statistics for Hackers
jakevdp
797
220k
Bootstrapping a Software Product
garrettdimon
PRO
306
110k
A designer walks into a library…
pauljervisheath
205
24k
Evolution of real-time – Irina Nazarova, EuRuKo, 2024
irinanazarova
6
570
Into the Great Unknown - MozCon
thekraken
35
1.6k
"I'm Feeling Lucky" - Building Great Search Experiences for Today's Users (#IAC19)
danielanewman
226
22k
How STYLIGHT went responsive
nonsquared
98
5.4k
Writing Fast Ruby
sferik
628
61k
Six Lessons from altMBA
skipperchong
27
3.6k
Adopting Sorbet at Scale
ufuk
74
9.2k
The Myth of the Modular Monolith - Day 2 Keynote - Rails World 2024
eileencodes
21
2.5k
Navigating Team Friction
lara
183
15k
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の引き継ぎは静的(コンパイル時)に解決される