|
| 1 | +--- |
| 2 | +title: 'Understanding TaskLocal in Swift Concurrency' |
| 3 | +author: 'Clive Liu' |
| 4 | +layout: post |
| 5 | +tags: [Swift, Concurrency] |
| 6 | +--- |
| 7 | + |
| 8 | +`TaskLocal` is a mechanism within Swift’s concurrency system that allows you to store and access values that are local to a task and its child tasks. It’s similar to thread-local storage from the pre-concurrency days, but tailored for Swift’s structured concurrency model. |
| 9 | + |
| 10 | +Imagine you’re writing code and want to pass contextual information—like a tracing context—throughout the call stack without explicitly passing it into every function. That’s exactly the kind of scenario `TaskLocal` is designed to simplify. Think of it like SwiftUI’s environment values: data flows from parent to child, and any descendant can access it without needing every function in the call chain to propagate it manually. |
| 11 | + |
| 12 | +## Usage |
| 13 | + |
| 14 | +Here’s how to define and use a task-local value: |
| 15 | + |
| 16 | +```swift |
| 17 | +enum MetricsReporter { |
| 18 | + // Must be declared as static properties (below Swift 6.0) |
| 19 | + @TaskLocal static var workflowID: UUID? |
| 20 | +} |
| 21 | + |
| 22 | +// Global task-local properties are supported starting with Swift 6.0 |
| 23 | +@TaskLocal var workflowID: UUID? |
| 24 | +``` |
| 25 | + |
| 26 | +To set a task-local value, you must use the `withValue` function: |
| 27 | + |
| 28 | +```swift |
| 29 | +await $workflowID.withValue(.init()) { |
| 30 | + await myAmazingWorkflow() |
| 31 | + |
| 32 | + Task { |
| 33 | + await myOtherAmazingWorkflow() |
| 34 | + } |
| 35 | + |
| 36 | + Task.detached { |
| 37 | + // Detached tasks do NOT inherit TaskLocal values. |
| 38 | + } |
| 39 | +} |
| 40 | +``` |
| 41 | + |
| 42 | +Within `myAmazingWorkflow()`, `myOtherAmazingWorkflow()`, and any functions they call, `workflowID` will return the value assigned via `withValue`. However, **detached tasks** do **not** inherit task-local values—they start with a clean slate. |
| 43 | + |
| 44 | +## Example |
| 45 | + |
| 46 | +Here’s a full example to demonstrate `TaskLocal` in action: |
| 47 | + |
| 48 | +```swift |
| 49 | +@TaskLocal var workflowID: UUID? |
| 50 | + |
| 51 | +func log(_ message: String) { |
| 52 | + print("\(workflowID) \(message)") |
| 53 | +} |
| 54 | + |
| 55 | +func step1() async { |
| 56 | + // do work |
| 57 | + log("step1 completed") |
| 58 | +} |
| 59 | + |
| 60 | +func step2() async { |
| 61 | + // do work |
| 62 | + log("step2 completed") |
| 63 | +} |
| 64 | + |
| 65 | +func step3() async { |
| 66 | + // do work |
| 67 | + log("step3 completed") |
| 68 | +} |
| 69 | + |
| 70 | +func myAmazingWorkflow() async { |
| 71 | + await step1() |
| 72 | + await step2() |
| 73 | + await step3() |
| 74 | +} |
| 75 | + |
| 76 | +Task { |
| 77 | + await withDiscardingTaskGroup { group in |
| 78 | + for _ in 0..<3 { |
| 79 | + group.addTask { |
| 80 | + await $workflowID.withValue(.init()) { |
| 81 | + await myAmazingWorkflow() |
| 82 | + } |
| 83 | + } |
| 84 | + } |
| 85 | + } |
| 86 | +} |
| 87 | +``` |
| 88 | + |
| 89 | +**Sample Output:** |
| 90 | + |
| 91 | +``` |
| 92 | +Optional(36A1DF29-CF82-4706-A268-B5056A3D83CA) step1 completed |
| 93 | +Optional(36A1DF29-CF82-4706-A268-B5056A3D83CA) step2 completed |
| 94 | +Optional(177104F2-15A8-437C-8114-69C5D88E7C09) step1 completed |
| 95 | +Optional(36A1DF29-CF82-4706-A268-B5056A3D83CA) step3 completed |
| 96 | +Optional(177104F2-15A8-437C-8114-69C5D88E7C09) step2 completed |
| 97 | +Optional(CA2DA6F3-DAA8-4F2A-AEBE-B5A6357723BF) step1 completed |
| 98 | +Optional(177104F2-15A8-437C-8114-69C5D88E7C09) step3 completed |
| 99 | +Optional(CA2DA6F3-DAA8-4F2A-AEBE-B5A6357723BF) step2 completed |
| 100 | +Optional(CA2DA6F3-DAA8-4F2A-AEBE-B5A6357723BF) step3 completed |
| 101 | +``` |
| 102 | + |
| 103 | +Each task gets its own `workflowID`, making it easy to correlate logs and trace execution flow—especially useful in debugging and monitoring. |
| 104 | + |
| 105 | +## Conclusion |
| 106 | + |
| 107 | +`TaskLocal` is a powerful tool in Swift’s concurrency toolbox. When used wisely, it can simplify your code by removing the need to pass contextual parameters everywhere. |
| 108 | + |
| 109 | +But like any powerful abstraction, it should be used with care. Make sure you're not hiding too much logic behind implicit context, and use it where it truly improves readability and maintainability. |
0 commit comments