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.7k
タイミー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
370
iOSエンジニアの為のgrpc-swift入門
_tanako
6
4.7k
grpc-swiftの紹介
_tanako
0
1.7k
How to write basic unittests
_tanako
0
180
Other Decks in Programming
See All in Programming
なまけものオバケたち -PHP 8.4 に入った新機能の紹介-
tanakahisateru
1
140
生成AIでGitHubソースコード取得して仕様書を作成
shukob
0
590
快速入門可觀測性
blueswen
0
470
AppRouterを用いた大規模サービス開発におけるディレクトリ構成の変遷と問題点
eiganken
1
380
非ブラウザランタイムとWeb標準 / Non-Browser Runtimes and Web Standards
petamoriken
0
410
ドメインイベント増えすぎ問題
h0r15h0
2
530
GitHubで育つ コラボレーション文化 : ニフティでのインナーソース挑戦事例 - 2024-12-16 GitHub Universe 2024 Recap in ZOZO
niftycorp
PRO
0
960
見えないメモリを観測する: PHP 8.4 `pg_result_memory_size()` とSQL結果のメモリ管理
kentaroutakeda
0
870
今年のアップデートで振り返るCDKセキュリティのシフトレフト/2024-cdk-security-shift-left
tomoki10
0
320
KMP와 kotlinx.rpc로 서버와 클라이언트 동기화
kwakeuijin
0
270
LLM Supervised Fine-tuningの理論と実践
datanalyticslabo
8
1.8k
ChatGPT とつくる PHP で OS 実装
memory1994
PRO
3
160
Featured
See All Featured
GraphQLの誤解/rethinking-graphql
sonatard
68
10k
GraphQLとの向き合い方2022年版
quramy
44
13k
Fantastic passwords and where to find them - at NoRuKo
philnash
50
2.9k
Designing Experiences People Love
moore
139
23k
Measuring & Analyzing Core Web Vitals
bluesmoon
5
190
No one is an island. Learnings from fostering a developers community.
thoeni
19
3.1k
How STYLIGHT went responsive
nonsquared
96
5.3k
Become a Pro
speakerdeck
PRO
26
5.1k
A Modern Web Designer's Workflow
chriscoyier
693
190k
Documentation Writing (for coders)
carmenintech
67
4.5k
We Have a Design System, Now What?
morganepeng
51
7.3k
Learning to Love Humans: Emotional Interface Design
aarron
274
40k
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の引き継ぎは静的(コンパイル時)に解決される