You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: proposals/0417-task-executor-preference.md
+26-31Lines changed: 26 additions & 31 deletions
Original file line number
Diff line number
Diff line change
@@ -422,37 +422,23 @@ and wrapping the code that is required to run on a specific executor in an `with
422
422
423
423
Nevertheless, because we understand there may be situations where synchronous code may want to compare task executors, this capability is exposed for advanced use cases.
424
424
425
-
Another use case may be carrying the same task executor into an un-structured Task -- although this should only be done with **extreme caution**,
426
-
because it breaks structured concurrency lifetime expectations of executors. For example, the following code is correct under structured concurrency's
427
-
default and automatic behavior surrounding task executors:
425
+
### TaskExecutor ownership
428
426
429
-
```swift
430
-
funccomputeThings() async {
431
-
let eventLoop =MyCoolEventLoop()
432
-
defer { eventLoop.shutdown() }
427
+
Task executors, unlike serial executors, are explicitly owned by tasks as long as they are running on the given task executor.
433
428
434
-
let computed =awaitwithTaskExecutorPreference(eventLoop) {
435
-
asynclet first =computation(1)
436
-
asynclet second =computation(2)
437
-
returnawait first + second
438
-
}
429
+
This is achieved in two ways. The `withTaskExecutorPreference` APIs by their construction as `with...`-style APIs,
430
+
naturally retain and keep alive the task executor foras long as the `with... { ... }` body is executing.
431
+
This also naturally extends to other structured concurrency constructs like `asynclet` and task groups, which can
432
+
rely on the task executor to remain alive while these constructs are running within such `withTaskExecutorPreference(...) { ... }` closure body.
439
433
440
-
return computed // event loop will be shutdown and the executor destroyed(!)
441
-
}
434
+
Unstructured tasks which are started with a task executor preference (e.g. `Task(executorPreference: someTaskExecutor)`),
435
+
take ownership of the executor foras long as the task is running.
442
436
443
-
funccomputation(_int: Int) ->Int { return int *2 }
444
-
```
445
-
446
-
The above code is structurally correct and we guarantee the lifetime of `MyCoolEventLoop` throughout all of its uses
447
-
by structured concurrency tasks in this snippet.
448
-
449
-
The following snippet is**not safe**, which is why task executors are not inherited to un-structured tasks:
437
+
In other words, it is safe to rely on a task, structured or not, to keep alive the task executor it may be running on.
438
+
This makes it possible to write code like the following snippet, without having to worry about manually keeping the
439
+
executor alive until "all tasks which may be executing on it have finished":
450
440
451
441
```swift
452
-
// !!! POTENTIALLY UNSAFE !!!
453
-
// Do not do this, unless you can guarantee the lifetime of TaskExecutor
454
-
// exceeds all potential for any task to be running on it (!)
455
-
456
442
funccomputeThings() async {
457
443
let eventLoop: any TaskExecutor =MyCoolEventLoop()
return computed // event loop will be shutdown and the executor destroyed(!)
467
453
}
468
454
469
-
// DANGEROUS; MUST ENSURE THE EXECUTOR REMAINS ALIVE FOR AS LONG AS ANY TASK MAY BE RUNNING ON IT
470
455
funccomputation(_int: Int) ->Int {
471
456
withUnsafeCurrentTask { task in
472
457
let unownedExecutor: UnownedTaskExecutor?= task?.unownedTaskExecutor
473
-
let eventLoop: MyCoolEventLoop?= EventLoops.find(unownedExecutor)
474
-
475
-
// Dangerous because there is no structured guarantee that eventLoop will be kept alive
476
-
// for as long as there are any of its child tasks and functions running on it
477
-
Task(executorPreference: eventLoop) { ... }
458
+
let eventLoop: MyCoolEventLoop = EventLoops.find(unownedExecutor)
459
+
// we need to start an unstructured task for some reason (try to avoid this if possible)
460
+
// and we have located the `MyCoolEventLoop` in our "cache".
461
+
//
462
+
// Since we have a real MyCoolEventLoop reference, this is safe to forward
463
+
// to the unstructured task which will retain it.
464
+
Task(executorPreference: eventLoop) {
465
+
asynclet something =...// inherits the executor preference
466
+
}
478
467
}
479
468
}
480
469
```
481
470
471
+
Same as with `SerialExecutor`'s `UnownedSerialExecutor` type, the `UnownedTaskExecutor` does _not_ retain the executor,
472
+
so you have to be extra careful when relying on unowned task executor references forany kind of operations. If you
473
+
were to write some form of "lookup" function, which takes an unowned executor and returns an `any TaskExecutor`,
474
+
please make sure that the returned references are alive (i.e. by keeping them alive in the "cache" using strong references).
475
+
482
476
## Combining `SerialExecutor` and `TaskExecutor`
483
477
484
478
It is possible to declare a single executor type and have it conform to *both* the `SerialExecutor` (introduced in the custom actor executors proposal),
@@ -786,6 +780,7 @@ We considered if not introducing this feature could be beneficial and forcing de
786
780
787
781
788
782
## Revisions
783
+
789
784
-1.6
790
785
- introduce the global `var defaultConcurrentExecutor:any TaskExecutor` we we can express a task specifically wanting to run on the default global concurrency pool.
0 commit comments