Skip to content

Commit 165df19

Browse files
committed
further clarify the handling of NIL
1 parent e97310c commit 165df19

File tree

1 file changed

+18
-12
lines changed

1 file changed

+18
-12
lines changed

proposals/0417-task-executor-preference.md

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -270,15 +270,15 @@ Which allows users to require child tasks be enqueued and run on specific execut
270270
Task(executorPreference: specialExecutor) {
271271
_ = await withTaskGroup(of: Int.self) { group in
272272
group.addTask {
273-
// using 'specialExecutor'
273+
// using 'specialExecutor' (inherited preference)
274274
return 12
275275
}
276276
group.addTask(executorPreference: differentExecutor) {
277-
// using 'differentExecutor'
277+
// using 'globalConcurrentExecutor', overriden preference
278278
return 42
279279
}
280280
group.addTask(executorPreference: nil) {
281-
// using 'specialExecutor'
281+
// using 'specialExecutor' (inherited preference)
282282
//
283283
// explicitly documents that this task has "no task executor preference".
284284
// this is semantically equivalent to the addTask() call without specifying
@@ -287,7 +287,7 @@ Task(executorPreference: specialExecutor) {
287287
return 84
288288
}
289289
group.addTask(executorPreference: globalConcurrentExecutor) {
290-
// using 'globalConcurrentExecutor'
290+
// using 'globalConcurrentExecutor', overriden preference
291291
//
292292
// using the global concurrent executor -- effectively overriding
293293
// the task executor preference set by the outer scope back to the
@@ -310,14 +310,14 @@ We propose adding new APIs and necessary runtime changes to allow a Task to be e
310310
extension Task where Failure == Never {
311311
@discardableResult
312312
public init(
313-
on taskExecutorPreference: (any TaskExecutor)?,
313+
executorPreference taskExecutor: (any TaskExecutor)?,
314314
priority: TaskPriority? = nil,
315315
operation: @Sendable @escaping () async -> Success
316316
)
317317

318318
@discardableResult
319319
static func detached(
320-
on taskExecutorPreference: any TaskExecutor,
320+
executorPreference taskExecutor: (any TaskExecutor)?,
321321
priority: TaskPriority? = nil,
322322
operation: @Sendable @escaping () async -> Success
323323
)
@@ -326,14 +326,14 @@ extension Task where Failure == Never {
326326
extension Task where Failure == Error {
327327
@discardableResult
328328
public init(
329-
on taskExecutorPreference: any TaskExecutor,
329+
executorPreference taskExecutor: (any TaskExecutor)?,
330330
priority: TaskPriority? = nil,
331331
operation: @Sendable @escaping () async throws -> Success
332332
)
333333

334334
@discardableResult
335335
static func detached(
336-
on taskExecutorPreference: (any TaskExecutor)?,
336+
executorPreference taskExecutor: (any TaskExecutor)?,
337337
priority: TaskPriority? = nil,
338338
operation: @Sendable @escaping () async throws -> Success
339339
)
@@ -342,6 +342,8 @@ extension Task where Failure == Error {
342342

343343
Tasks created this way are **immediately enqueued** on given executor.
344344

345+
It is possible to pass `nil` to all task executor accepting APIs introduced in this proposal. Passing `nil` to an `executorPreference:` parameter means "no preference", and for structured tasks means to inherit the surrounding context's executor preference; and for unstructured tasks (`Task.init`, `Task.detached`) it serves as a way of documenting no specific executor preference was selected for this task. In both cases, passing `nil` is equivalent to calling the methods which do not accept an executor preference.
346+
345347
By default, serial executors are not task executors, and therefore cannot be directly used with these APIs.
346348
This is because it would cause confusion in the runtime about having two "mutual exclusion" contexts at the same time, which could result in difficult to understand behaviors.
347349

@@ -370,7 +372,10 @@ however isolation is still guaranteed by the actor's semantics:
370372
let anywhere = RunsAnywhere()
371373
Task { await anywhere.hello() } // runs on "default executor", using a thread from the global pool
372374

373-
Task(executorPreference: myExecutor) { await anywhere.hello() } // runs on preferred executor, using a thread owned by that executor
375+
Task(executorPreference: myExecutor) {
376+
// runs on preferred executor, using a thread owned by that executor
377+
await anywhere.hello()
378+
}
374379
```
375380

376381
Methods which assert isolation, such as `Actor/assumeIsolated` and similar still function as expected.
@@ -684,12 +689,13 @@ In other words, task executor preference gives control to developers at when and
684689

685690
The default of hop-avoiding when a preference is set is also a good default because it optimizes for less context switching and can lead to better performance.
686691

687-
It is possible to disable a preference by setting the preference to `nil`. So if we want to make sure that some code would not be influenced by a caller's preference, we can defensively insert the following:
692+
It is possible to effectively restore the default behavior as-if no task executor preference was present, by setting the preference to the `globalConcurrentExecutor` which is the executor used by default actors, tasks, and free async functions when no task executor preference is set:
688693

689694
```swift
690695
func function() async {
691-
// make sure to ignore caller's task executor preference
692-
await withTaskExecutorPreference(nil) { ... }
696+
// make sure to ignore caller's task executor preference,
697+
// and always use the global concurrent executor.
698+
await withTaskExecutorPreference(globalConcurrentExecutor) { ... }
693699
}
694700
```
695701

0 commit comments

Comments
 (0)