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.9k
タイミー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
390
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
設計の本質:コード、システム、そして組織へ / The Essence of Design: To Code, Systems, and Organizations
nrslib
10
3.7k
RubyKaigi Dev Meeting 2025
tenderlove
1
1.3k
今話題のMCPサーバーをFastAPIでサッと作ってみた
yuukis
0
110
Bedrock × Confluenceで簡単(?)社内RAG
iharuoru
1
110
The Implementations of Advanced LR Parser Algorithm
junk0612
1
1.3k
2025-04-25 GitHub Copilot Agent ライブデモ(スクリプト)
goataka
0
100
GitHub Copilot for Azureを使い倒したい
ymd65536
1
300
Носок на сок
bo0om
0
1.1k
iOSアプリで測る!名古屋駅までの 方向と距離
ryunakayama
0
150
Enterprise Web App. Development (1): Build Tool Training Ver. 5
knakagawa
1
120
Creating Awesome Change in SmartNews! En
martin_lover
0
110
note の Elasticsearch 更新系を支える技術
tchov
9
3.4k
Featured
See All Featured
Fight the Zombie Pattern Library - RWD Summit 2016
marcelosomers
233
17k
Templates, Plugins, & Blocks: Oh My! Creating the theme that thinks of everything
marktimemedia
30
2.3k
A better future with KSS
kneath
239
17k
Producing Creativity
orderedlist
PRO
344
40k
Git: the NoSQL Database
bkeepers
PRO
430
65k
Music & Morning Musume
bryan
47
6.5k
Practical Tips for Bootstrapping Information Extraction Pipelines
honnibal
PRO
19
1.2k
10 Git Anti Patterns You Should be Aware of
lemiorhan
PRO
656
60k
StorybookのUI Testing Handbookを読んだ
zakiyama
29
5.7k
Fashionably flexible responsive web design (full day workshop)
malarkey
407
66k
The Invisible Side of Design
smashingmag
299
50k
実際に使うSQLの書き方 徹底解説 / pgcon21j-tutorial
soudai
178
53k
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の引き継ぎは静的(コンパイル時)に解決される