Skip to content

Commit e547325

Browse files
authored
Read TLS directly if possible (#38)
1 parent 728388f commit e547325

File tree

1 file changed

+39
-41
lines changed

1 file changed

+39
-41
lines changed

Sources/NodeAPI/NodeActor.swift

Lines changed: 39 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -39,47 +39,45 @@ private final class NodeExecutor: SerialExecutor {
3939
}
4040

4141
func enqueue(_ job: UnownedJob) {
42-
schedulerQueue.async {
43-
// We want to access `job`'s task-local storage. To do so,
44-
// this temporarily swaps ResumeTask for our own function.
45-
// Then, swift_job_run is called, which sets the active task to
46-
// the receiver and invokes its ResumeTask. We then execute the
47-
// given closure, allowing us to grab task-local values. Finally,
48-
// we "suspend" the task and return ResumeTask to its old value.
49-
//
50-
// on Darwin we can instead replace the "current task" thread-local
51-
// (key 103) temporarily, but that isn't portable.
52-
//
53-
// This is sort of like inserting a "work(); await Task.yield()" block
54-
// at the top of the task, since when a Task awaits it similarly changes
55-
// the Resume function and suspends. Note that we can assume that this
56-
// is a Task and not a basic Job, because Executor.enqueue is only
57-
// called from swift_task_enqueue.
58-
//
59-
// Regarding `schedulerQueue.async`:
60-
// Pre Swift 6.0 we didn't need a scheduler queue as enqueue would always
61-
// run on the global queue. However, Swift 6 introduces optimizations in
62-
// Task dispatch that allow tasks to be enqueued more efficiently, including
63-
// that Task.init avoids a hop when possible. This, however, interferes with
64-
// our `job.asCurrent` code because `asCurrent` relies on there being no already-
65-
// running task (swift_job_run doesn't play well with nesting, it's possible but
66-
// requires more private APIs, cf [swift_task_startOnMainActor][1]). The simplest
67-
// solution is to hop onto our own queue for scheduling.
68-
//
69-
// [1]: https://github.com/apple/swift/blob/876c056153554f93b89dfd134794a05426ee789a/stdlib/public/Concurrency/Task.cpp#L1739
70-
let target = job.asCurrent { NodeActor.target }
71-
72-
guard let q = target?.queue else {
73-
nodeFatalError("There is no target NodeAsyncQueue associated with this Task")
74-
}
75-
76-
let ref = self.asUnownedSerialExecutor()
77-
78-
do {
79-
try q.run { job.runSynchronously(on: ref) }
80-
} catch {
81-
nodeFatalError("Could not execute job on NodeActor: \(error)")
82-
}
42+
// We want to enqueue the job on the "current" NodeActor, which is
43+
// stored in task-local storage. In the Swift 6 compiler,
44+
// NodeExecutor.enqueue is invoked with the same isolation as the caller,
45+
// which means we can simply read out the TaskLocal value to obtain
46+
// this.
47+
let target: NodeAsyncQueue.Handle?
48+
#if compiler(>=6.0)
49+
target = NodeActor.target
50+
#else
51+
// It's a bit trickier in Swift <= 5.10 as Swift first makes a hop to the
52+
// global executor before invoking this method. So instead we have to use
53+
// some runtime spelunking to read the TaskLocal value from `job`.
54+
// To do so, we temporarily swap ResumeTask for our own function.
55+
// Then, swift_job_run is called, which sets the active task to
56+
// the receiver and invokes its ResumeTask. We then execute the
57+
// given closure, allowing us to grab task-local values. Finally,
58+
// we "suspend" the task and return ResumeTask to its old value.
59+
//
60+
// on Darwin we can instead replace the "current task" thread-local
61+
// (key 103) temporarily, but that isn't portable.
62+
//
63+
// This is sort of like inserting a "work(); await Task.yield()" block
64+
// at the top of the task, since when a Task awaits it similarly changes
65+
// the Resume function and suspends. Note that we can assume that this
66+
// is a Task and not a basic Job, because Executor.enqueue is only
67+
// called from swift_task_enqueue.
68+
target = job.asCurrent { NodeActor.target }
69+
#endif
70+
71+
guard let q = target?.queue else {
72+
nodeFatalError("There is no target NodeAsyncQueue associated with this Task")
73+
}
74+
75+
let ref = self.asUnownedSerialExecutor()
76+
77+
do {
78+
try q.run { job.runSynchronously(on: ref) }
79+
} catch {
80+
nodeFatalError("Could not execute job on NodeActor: \(error)")
8381
}
8482
}
8583

0 commit comments

Comments
 (0)