Recently, I came across an interesting concurrency challenge on LinkedIn, specifically about Structured Concurrency in Swift.
The challenge was to predict the order in which the following prints would appear in the console: Print 1, Print 2, Print 3, and Print 4.
class TaskManager {
@TaskLocal static var message = "Hello"
static func run() {
Self.$message
.withValue("Bye") {
print("Print 1: \(Self.message)")
Task {
print("Print 2: \(Self.message)")
}
Task.detached {
print("Print 3: \(Self.message)")
}
}
print("Print 4: \(Self.message)")
}
}
What Is @TaskLocal?
@TaskLocal
allows you to define a value that is accessible only within a specific task and its child tasks. Here’s a simple example:
@TaskLocal static var userID: String?
func printUserID() async {
if let id = userID {
print("User ID: \(id)")
} else {
print("No user ID")
}
}
Task {
await Self.userID.withValue("12345") {
await printUserID() // User ID: 12345
}
await printUserID() // No user ID
}
Expected Behavior
I expected the following print order: Print 1, Print 4, Print 2, Print 3.
This seemed reasonable because Task.detached runs with .medium priority by default, so it might execute after the regular Task. However, the actual behavior depends on the context in which TaskManager.run()
is called.
Playground Experiment
I first ran the code in Playground.
Most of the time, I got this output: Print 1, Print 2, Print 3, Print 4.
Occasionally, the order changed to: Print 1, Print 2, Print 4, Print 3.
This behavior seemed strange, so I decided to check how it performs in more realistic conditions.
XCTest Verification
I wrote a test to run TaskManager.run()
1000 times:
final class TaskManagerTests: XCTestCase {
func test_taskManager() async {
for i in 1...1000 {
print("\n--- Run \(i) ---")
TaskManager.run()
try? await Task.sleep(nanoseconds: 500_000_000)
}
}
}
The behavior matched my expectations: Print 1, Print 4, Print 2, Print 3.
I also noticed occasional variations where Print 2 and Print 3 would switch places.
To better understand the behavior, I decided to inspect task priorities by adding logs for Task.currentPriority
.
Investigating Task Priorities
To better understand this behavior, I added Task.currentPriority
logging inside both Task { }
and Task.detached { }
.
In XCTest, the priority turned out to be .medium
, resulting in stable and predictable behavior.
In Playground, the priority was .high
, which explains why Task.detached
(with its default .medium
priority) consistently ran later.
However, Print 4 also consistently appeared last, and while the exact reason remains unclear, it seems to be specific to how execution is managed in Playground.
In more realistic environments like XCTest, the behavior was more consistent and aligned with expectations.
Experiment with Lower Priority
Next, I decided to wrap the test in a low-priority root task to see how that would affect the execution order:
func test_that_run_executesCorrectly_multipleTimes() async {
await Task.detached(priority: .background) {
for i in 1...1000 {
print("\n--- Run \(i) ---")
TaskManager.run()
try? await Task.sleep(nanoseconds: 500_000_000)
}
}.value
}
With this change, the output shifted slightly, producing: Print 1, Print 4, Print 3, Print 2. This shows that lowering the priority of the root task consistently causes Print 3 to appear before Print 2.
Conclusion
The behavior of tasks in Playground and in a real application differs. In Playground, tasks run with a .high
priority by default, which affects their execution order.
In XCTest, tasks run with .medium
priority, resulting in more stable and predictable behavior. The actual execution order in a production app depends on the execution context - whether it’s Task
, Task.detached
, or other contexts.
@TaskLocal
correctly isolates values for tasks, but you should always verify behavior in environments that closely resemble production, not just in Playground.
If you want to ensure your code behaves as expected — test it not only in Playground but also in your actual app or test suite. This will help you avoid misleading results and unexpected behavior in production.