1111
1212Swift's general philosophy is to prioritize safety and ease-of-use over
1313performance, while still providing tools to write more efficient code. The
14- current behavior of nonisolated async functions prioritizes performance at the
15- expense of usability.
14+ current behavior of nonisolated async functions prioritizes main actor
15+ responsiveness at the expense of usability.
1616
1717This proposal changes the behavior of nonisolated async functions to inherit
1818the isolation of the caller, and introduces an explicit way to state that an
@@ -39,6 +39,7 @@ manner.
3939- [ Implications on adoption] ( #implications-on-adoption )
4040- [ Alternatives considered] ( #alternatives-considered )
4141 - [ Different spelling for ` @concurrent ` ] ( #different-spelling-for-concurrent )
42+ - [ Use ` nonisolated ` instead of a separate ` @concurrent ` attribute] ( #use-nonisolated-instead-of-a-separate-concurrent-attribute )
4243 - [ Don't introduce a type attribute for ` @concurrent ` ] ( #dont-introduce-a-type-attribute-for-concurrent )
4344
4445## Motivation
@@ -53,7 +54,7 @@ preventing unexpected overhang on the main actor.
5354
5455This decision has a number of unfortunate consequences.
5556
56- ** ` nonisolated ` is difficult to understand.** There is a semantic difference
57+ ** ` nonisolated ` is difficult to understand.** There is a semantic difference
5758between the isolation behavior of nonisolated synchronous and asynchronous
5859functions; nonisolated synchronous functions always stay in the isolation
5960domain of the caller, while nonisolated async functions always switch off of
@@ -117,7 +118,7 @@ only manifests when calling the API from an actor.
117118
118119The concurrency library itself has made this mistake, and many of the async
119120APIs in the concurrency library have since transitioned to inheriting the
120- isolation of the caller using isolated parameters; see
121+ isolation of the caller using isolated parameters; see
121122[ SE-0421] ( /proposals/0421-generalize-async-sequence.md ) for an example.
122123
123124** It's difficult to write higher-order async APIs.** Consider the following
@@ -165,8 +166,30 @@ complicated.
165166I propose changing nonisolated async functions to inherit the isolation of the
166167caller by default. This means that nonisolated functions always have the same
167168isolation rules, regardless of whether the function is synchronous or
168- asynchronous. The ` @concurrent ` declaration attribute can be used to opt into
169- async functions always running concurrently with actors.
169+ asynchronous. This makes the following example from the motivation section
170+ valid, because the call to ` x.performAsync() ` does not cross an isolation
171+ boundary:
172+
173+ ``` swift
174+ class NotSendable {
175+ func performSync () { ... }
176+ func performAsync () async { ... }
177+ }
178+
179+ actor MyActor {
180+ let x: NotSendable
181+
182+ func call () async {
183+ x.performSync () // okay
184+
185+ await x.performAsync () // okay
186+ }
187+ }
188+ ```
189+
190+ This proposal also introduces the ` @concurrent ` declaration attribute to opt
191+ out of isolation inheritance, so that the function always switches off of an
192+ actor to run.
170193
171194## Detailed design
172195
@@ -204,7 +227,7 @@ The implicit parameter is not preserved when using a nonisolated async function
204227as a value. When referencing a nonisolated async function unapplied in a
205228context that expects a nonisolated ` @Sendable ` or ` sending ` function type, the
206229function will switch off of the caller's actor when the function value is
207- called.
230+ called, and sendable checking will be applied to argument and result values .
208231
209232> Note: It is not feasible to implicitly add parameters to function values
210233> without widespread ABI impact. It's possible to stage in an ABI change for
@@ -214,11 +237,18 @@ called.
214237For example:
215238
216239``` swift
217- func useAsValue () async {}
240+ class NotSendable { ... }
241+
242+ func useAsValue (_ ns : NotSendable) async { ... }
243+
244+ @MainActor let global: NotSendable = .init ()
218245
219246@MainActor
220- func callSendableClosure (closure : @Sendable () async -> Void ) {
221- await closure ()
247+ func callSendableClosure (closure : @Sendable (NotSendable) async -> Void ) {
248+ let ns = NotSendable ()
249+ await closure (ns) // okay
250+
251+ await closure (global) // error
222252}
223253
224254callSendableClosure (useAsValue)
@@ -270,7 +300,8 @@ actor MyActor {
270300
271301It is an error to use ` @concurrent ` together with another form of isolation,
272302including global actors, isolated parameters, ` nonisolated ` , and
273- ` @isolated(any) ` .
303+ ` @isolated(any) ` . ` @concurrent ` can be used together with ` @Sendable ` or
304+ ` sending ` .
274305
275306### Task isolation inheritance
276307
@@ -386,30 +417,71 @@ isolation for a closure depends on two factors:
3864172 . Whether the contextual type of the closure is ` @Sendable ` or ` sending ` .
387418
388419If the contextual type of the closure is neither ` @Sendable ` nor ` sending ` , the
389- inferred isolation of the closure is the same as the enclosing context. If
390- either the type of the closure is ` @Sendable ` or the closure is passed to a
391- ` sending ` parameter, the closure is inferred to be nonisolated:
420+ inferred isolation of the closure is the same as the enclosing context:
392421
393422``` swift
394423class NotSendable { ... }
395424
396425@MainActor
397- func closureOnMain (ns : NotSendable) {
398- let nonSendableClosure : () -> Void = {
426+ func closureOnMain (ns : NotSendable) async {
427+ let syncClosure : () -> Void = {
399428 // inferred to be @MainActor-isolated
400429
401430 // capturing main-actor state is okay
402431 print (ns)
403432 }
404433
405- let sendableClosure: @Sendable () -> Void = {
434+ // runs on the main actor
435+ syncClosure ()
436+
437+ let asyncClosure: (NotSendable) async -> Void = {
438+ // inferred to be @MainActor-isolated
439+
440+ print ($0 )
441+ }
442+
443+ // runs on the main actor;
444+ // passing main-actor state is okay
445+ await asyncClosure (ns)
446+ }
447+ ```
448+
449+ If either the type of the closure is ` @Sendable ` or the closure is passed to a
450+ ` sending ` parameter, the closure is inferred to be ` nonisolated ` . If the
451+ closure is ` async ` , the closure will switch off of the calling actor to run:
452+
453+ ``` swift
454+ class NotSendable { ... }
455+
456+ @MainActor
457+ func closureOnMain (ns : NotSendable) {
458+ let syncClosure: @Sendable () -> Void = {
406459 // inferred to be nonisolated
407460
408461 print (ns) // error
409462 }
463+
464+ let asyncClosure: @Sendable (NotSendable) async -> Void = {
465+ // inferred to be nonisolated and runs off of the actor
466+
467+ print ($0 )
468+ }
469+
470+ await asyncClosure (ns) // error
410471}
411472```
412473
474+ ** Open question.** The current compiler implementation does not implicitly
475+ capture the isolation of the enclosing context for async closures formed in a
476+ method with an isolated parameter; the closure is only isolated to the actor if
477+ the actor value is explicitly captured. This is done to avoid implicitly
478+ capturing values that are invisible to the programmer, because this can lead to
479+ reference cycles. This behavior is surprising with respect to data-race safety,
480+ but I'm concerned about changes to this behavior causing new memory leaks. One
481+ potential compromise is to keep the current isolation inference behavior, and
482+ offer fix-its to capture the actor if there are any data-race safety errors
483+ from capturing state in the actor's region.
484+
413485### Function conversions
414486
415487A function conversion that changes the isolation of a function value applies
@@ -659,9 +731,13 @@ class NotSendable { ... }
659731### Executor switching
660732
661733Async functions switch executors in the implementation when entering the
662- function, and after any calls to async functions. Isolated functions switch to
663- the isolated parameter or global actor's executor, and ` @concurrent ` functions
664- switch to the generic executor:
734+ function, and after any calls to other async functions. Note that synchronous
735+ functions do not have the ability to switch executors, and if a call to a
736+ synchronous function crosses an isolation boundary, the call must happen in an
737+ async context and the executor switch happens at the caller.
738+
739+ ` @concurrent ` async functions switch to the generic executor, and all other
740+ async functions switch to the isolated actor's executor.
665741
666742``` swift
667743@MainActor func runOnMainExecutor () async {
@@ -675,23 +751,46 @@ switch to the generic executor:
675751@concurrent func runOnGenericExecutor () async {
676752 // switch to generic executor
677753
678- await Task { @MainActor in ... }.value
754+ await Task { @MainActor in
755+ // switch to main actor executor
756+
757+ ...
758+ }.value
679759
680760 // switch to generic executor
681761}
682762```
683763
684- Executor switching behaves the same way in nonisolated async functions:
764+ Under this proposal, by default, nonisolated async functions will switch to
765+ the executor of the implicit isolated parameter instead of switching to the
766+ generic executor:
685767
686768``` swift
687- @MainActor func runOnMainExecutor () { ... }
769+ @MainActor func runOnMainExecutor () async {
770+ // switch to main actor executor
771+ ...
772+ }
773+
774+ class NotSendable {
775+ var value = 0
776+ }
688777
689- nonisolated func inheritIsolation () async {
778+ actor MyActor {
779+ let ns: NotSendable = .init ()
780+
781+ func callNonisolatedFunction () async {
782+ await inheritIsolation (ns)
783+ }
784+ }
785+
786+ nonisolated func inheritIsolation (_ ns : NotSendable) async {
690787 // switch to isolated parameter's executor
691788
692789 await runOnMainExecutor ()
693790
694791 // switch to isolated parameter's executor
792+
793+ ns.value += 1
695794}
696795```
697796
0 commit comments