Created
January 17, 2024 04:45
-
-
Save nkmrh/d8d3f87973094d578b4176eda3bb54a0 to your computer and use it in GitHub Desktop.
TaskLocal
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import UIKit | |
/*: | |
# TaskLocal | |
# 背景 | |
- Swiftが非同期関数とアクターを採用したことで、非同期コードはどこにでも存在する | |
- 非同期コードをデバッグ、トレース、その他の方法でインスツルメンテーションする必要性は、以前よりもさらに必要になる | |
- スレッドローカルやキュー固有の値など、リクエストに沿って情報を運ぶためにインスツルメンテーションシステムが以前使用していたツールは、Swiftのタスクに焦点を当てた並行処理ともはや互換性がない | |
# モチベーション | |
[Swift の分散トレーシング](https://github.com/apple/swift-distributed-tracing) ライブラリでスレッドローカルを使って実装しようとしたが、スレッドローカルを正しく使用することは非常に難しくエラーが発生しやすい | |
# TaskLocal | |
タスクで追加情報を運ぶことがきるように、ライブラリとインストルメンテーションの作者のために、Swift APIを介して、「メタデータを運ぶ」タスクの内部機能を公開することを提案したもの。 | |
現状、似たような振る舞いをするタスクAPIには、具体的に次のようなものがある:`Task.currentPriority` と `Task.isCancelled` | |
TaskLocal は、`静的ストアドプロパティ`として宣言され、`@TaskLocal` プロパティラッパーを使ってアノテーションする | |
*/ | |
enum MyLibrary { | |
@TaskLocal | |
static var requestID: String? | |
} | |
/*: | |
注意: プロパティラッパーは現在、グローバル宣言では使用できません。 これが変更されれば、トップレベルのTaskLocalを宣言できるようになる。 | |
TaskLocalプロパティの値へのアクセスは、プロパティにアクセスすることで可能。 | |
タスクのローカル値は、タスクの内部で実行されているにもかかわらず、非同期関数からも同期関数からも同じAPIを使ってアクセスできる。 | |
非同期関数は、現在のタスクがあることが保証されているため、常に現在のタスクの内部で検索を実行する。 | |
一方、同期関数はタスクコンテキスト内から呼び出されなかった場合、単にデフォルト値を返す。 | |
*/ | |
func asyncPrintRequestID() async { | |
let id = MyLibrary.requestID | |
print(id ?? "no-request-id") | |
} | |
func syncPrintRequestID() { // also works in synchronous functions | |
let id = MyLibrary.requestID | |
print(id ?? "no-request-id") | |
} | |
//Task { | |
// await asyncPrintRequestID() | |
//} | |
// | |
//syncPrintRequestID() | |
/*: | |
TaskLocalの値検索は、静的プロパティの検索よりもコストがかかる。 | |
スレッドローカルでアクセスし、値が見つかるかスタックの終端に達するまで値のスタックをスキャンする。そのため、TaskLocal値の使用には注意が必要で、例えば、forループの外側に格納するなどして、可能な限り一度しか参照しないようにする必要がある。 | |
特定のTaskLocalを特定の値にバインドするには、TaskLocal プロパティラッパーで宣言されている`withValue(_:operation:)`関数を使う。 | |
*/ | |
//Task { | |
// await MyLibrary.$requestID.withValue("1234-5678") { | |
// await asyncPrintRequestID() // prints: 1234-5678 | |
// syncPrintRequestID() // prints: 1234-5678 | |
// } | |
//} | |
//Task { | |
// await asyncPrintRequestID() // prints: no-request-id | |
//} | |
//syncPrintRequestID() // prints: no-request-id | |
/*: | |
withValue操作は同期的に実行される。 | |
同じタスクで実行中に、同じキーを複数回バインドすることも可能である。 | |
次のように、最新のバインディングが前のバインディングをシャドーイングしていると考えることができる | |
*/ | |
//syncPrintRequestID() // prints: no-request-id | |
// | |
//MyLibrary.$requestID.withValue("1111") { | |
// syncPrintRequestID() // prints: 1111 | |
// | |
// MyLibrary.$requestID.withValue("2222") { | |
// syncPrintRequestID() // prints: 2222 | |
// } | |
// | |
// syncPrintRequestID() // prints: 1111 | |
//} | |
// | |
//syncPrintRequestID() // prints: no-request-id | |
/*: | |
TaskLocalは、どのようにネストされているかに関係なく、値を設定したコンテキ ストから呼び出されたすべての関数によって読み取り可能である。 | |
例えば、非同期関数が値を設定し、いくつかの非同期関数を経由して呼び出し、最後に1つの同期関数を呼び出すことが可能です。すべての関数は、このようにバインドされた値を読むことができる。 | |
*/ | |
func outer() async -> String? { | |
await MyLibrary.$requestID.withValue("1234") { | |
MyLibrary.requestID // "1234" | |
return await middle() // "1234" | |
} | |
} | |
func middle() async -> String? { | |
MyLibrary.requestID // "1234" | |
return inner() // "1234" | |
} | |
func inner() -> String? { // synchronous function | |
return MyLibrary.requestID // "1234" | |
} | |
//Task { | |
// await outer() | |
//} | |
/*: | |
タスクグループを使って子タスクを作成した場合、その子タスクは親タスクが外部スコープに設定したのと同じ値を継承して読み込むことになる | |
*/ | |
//Task { | |
// await MyLibrary.$requestID.withValue("1234-5678") { | |
// await withTaskGroup(of: String.self) { group in | |
// group.addTask { // add child task running this closure | |
// MyLibrary.requestID ?? "" // returns "1234-5678", which was bound by the parent task | |
// } | |
// | |
// return await group.next()! // returns "1234-5678" | |
// } // returns "1234-5678" | |
// } | |
//} | |
/*: | |
子タスクが親のTaskLocal値を変更することはできない。 | |
TaskLocalの値は、一連の非同期呼び出しを通じてメタデータを運ぶのに便利である。 | |
# 先行技術 | |
Kotlin: CoroutineContext[T] | |
Kotlinは、コルーチン "スコープ "と "コンテキスト "と対話するための明示的なAPIを提供し、これらの抽象化はSwiftのタスク抽象化と非常に似ている。 | |
明示的なCoroutineContextAPIが提供され、アクセス可能な任意の場所からコンテキストを読み取ることができる。 | |
使い方 | |
```kotlin | |
println("Running in ${coroutineContext[CoroutineName]}") | |
``` | |
ここで、CoroutineNameは Keyであり、コルーチン内で実行されると期待される名前が得られる。 | |
# 検討された代替案 | |
サーフェスAPI:型ベースのキー定義 | |
TaskLocalのキーを定義するために最初に提案されたアプローチは、型が常に一意であるおかげで、間違うことは不可能だった。しかし、キーの宣言と使用は、レビュー中にコミュニティから面倒くさすぎると判断されたため、現在は@TaskLocalプロパティのラッパーを提案している。 | |
以前の設計では、キーを宣言するためにこの定型文が必要だった: | |
```swift | |
extension TaskLocalValues { | |
public struct RequestIDKey: TaskLocalKey { | |
// alternatively, one may declare a nil default value: | |
// public static var defaultValue: String? { nil } | |
public static var defaultValue: String { "<no-request-id>" } | |
// additional options here, like e.g. | |
// static var inherit: TaskLocalValueInheritance = . never | |
} | |
public var requestID: RequestIDKey { .init() } | |
} | |
``` | |
使い方は次のようになる: | |
```swift | |
await Task.withLocal(\.requestID, boundTo: "abcd") { | |
_ = Task.local(\.requestID) // "abcd" | |
} | |
``` | |
この宣言はあまりにボイラープレート的であるため破棄され、プロパティラッパー・ベースのAPIに移行した。 | |
# 却下された代替案 | |
## スレッドローカル変数 | |
スレッドローカルストレージは、Swiftの並行性モデルでは効果的に機能しない。 | |
Swift の並行性モデルは、非同期関数が実行される特定のスレッドについての保証が行われないため、スレッド用語を使用することを意図的に避けてる。 | |
代わりに、タスクとエクゼキュータ(すなわち、タスクを実際に実行するスレッドプール、イベントループ、またはディスパッチキュー)の用語で表現される。 | |
言い換えればスレッドローカルは、Swiftの並行性モデルで効果的に動作することができない。 | |
なぜなら、このモデルは操作に使用される特定のスレッドについて何の保証もしないからである。 | |
## ディスパッチキュー固有の値 | |
ディスパッチは、ディスパッチ・キューに固有の値を設定できるAPIを提供している: | |
DispatchQueue.setSpecific(キー:値:) https://developer.apple.com/documentation/dispatch/dispatchqueue/2883699-setspecific | |
DispatchQueue.getSpecific(key:)。https://developer.apple.com/documentation/dispatch/dispatchqueue/1780751-getspecific | |
これらのAPIは目的を十分に果たすが、Swift Concurrencyのタスクに焦点を当てたモデルとは互換性がない。 | |
アクターと非同期関数がディスパッチキューで実行されるとしても、実行がキュー間を行き来する可能性があるため、Swift Concurrency でうまく動作するために必要な、複数のキューにわたって値を運ぶ機能は実現できない。 | |
# 想定される使用例 | |
TaskLocalの値は、明示的にパラメータを渡すことが適切である場合に、それを置き換えるためのものではない。 | |
TaskLocalのストレージは、明示的に渡されるパラメーターよりもアクセスコストが高いことに注意すること。 | |
呼び出されたときにTaskLocal値が設定されていなければならないようなAPIを誤って構築しないようにすること。 | |
```swift | |
// Do not like this | |
enum AppData { | |
@TaskLocal static var currentUserId: Int = 0 | |
} | |
class NetworkManager { | |
func fetchData() async { | |
print("\(AppData.currentUserId)") | |
// Additional network request logic... | |
} | |
} | |
func handleUserRequest(userId: Int) async { | |
await AppData.$currentUserId.withValue(userId) { | |
let networkManager = NetworkManager() | |
await networkManager.fetchData() | |
} | |
} | |
await handleUserRequest(userId: 1) // prints 1 | |
// ref: https://medium.com/@priyans05/understanding-tasklocal-in-swift-enhancing-task-specific-data-handling-9b91b6d95828 | |
``` | |
TaskLocal・ストレージは、補助的なメタデータや「実行スコープ付きコンフィギュレーション」(特定の呼び出しの間だけ実行時ビットをモックアウトするが、グローバルにはモックアウトしないなど)にのみ使用する。 | |
## 使用例 進捗モニタリング | |
インタラクティブなアプリケーションでは、非同期タスクは、タスクを待っているユーザーが、タスクが実際に進行していることを知ることができるように、何らかの進行状況インジケータとリンクしていることが多い。 | |
Foundation はタスクの進捗を簡単にユーザーに報告するために、SwiftUI などの UI フレームワークで使われるProgress型を提供する。 | |
現在、Progressは手動で明示的に渡すか、スレッドローカルストレージを通してアクセスすることで使用できる。 | |
子タスクは結局のところタスクの進捗に貢献する。子タスクは結局のところタスクの進捗に貢献するのだから。タスクのローカル値を使えば、タスクと子タスクで自然に動作する、進捗監視のためのすばらしいAPIを提供できる。 | |
## 使用例 分散トレースとコンテキストロギング | |
https://github.com/apple/swift-distributed-tracing | |
--- | |
ref: https://github.com/apple/swift-evolution/blob/main/proposals/0311-task-locals.md | |
*/ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment