Skip to content

Commit 8b0dde7

Browse files
committed
merge Matt Massicotte's "Parameter Actor Inheritance" proposal
1 parent 16e9bf0 commit 8b0dde7

File tree

1 file changed

+90
-10
lines changed

1 file changed

+90
-10
lines changed

proposals/nnnn-closure-isolation.md

Lines changed: 90 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,30 @@
11
# Closure isolation control
22

33
* Proposal: [SE-NNNN](nnnn-closure-isolation.md)
4-
* Authors: [Sophia Poirier](https://github.com/sophiapoirier), [John McCall](https://github.com/rjmccall)
4+
* Authors: [Sophia Poirier](https://github.com/sophiapoirier), [Matt Massicotte](https://github.com/mattmassicotte), [John McCall](https://github.com/rjmccall)
55
* Review Manager: TBD
6+
* Status: **Awaiting review**
67
* Implementation: On `main` gated behind `-enable-experimental-feature TODO`
78
* Previous Proposals: [SE-0313](0313-actor-isolation-control.md), [SE-0316](0316-global-actors.md)
8-
* Review: ([pitch](https://forums.swift.org/TODO))
9+
* Review: ([pitch](https://forums.swift.org/t/isolation-assumptions/69514))
910

1011
## Introduction
1112

12-
This proposal provides the ability to explicitly specify actor-isolation or non-isolation of a closure, as well as providing a parameter attribute to guarantee that a closure parameter inherits the isolation of the context.
13+
This proposal provides the ability to explicitly specify actor-isolation or non-isolation of a closure, as well as providing a parameter attribute to guarantee that a closure parameter inherits the isolation of the context. It makes the isolation inheritance rules more uniform while also making a specific concurrency pattern less-restrictive.
14+
15+
## Table of Contents
16+
17+
* [Introduction](#introduction)
18+
* [Motivation](#motivation)
19+
* [Proposed solution](#proposed-solution)
20+
* [Detailed design](#detailed-design)
21+
+ [Inheritance Change](#inheritance-hange)
22+
+ [Inheritance Opt-Out](#inheritance-opt-out)
23+
* [Source compatibility](#source-compatibility)
24+
* [ABI compatibility](#abi-compatibility)
25+
* [Implications on adoption](#implications-on-adoption)
26+
* [Alternatives considered](#alternatives-considered)
27+
* [Acknowledgments](#acknowledgments)
1328

1429
## Motivation
1530

@@ -26,7 +41,26 @@ Without a global actor isolation annotation, actor-isolation or non-isolation of
2641
* global actor
2742
* specific actor value
2843

29-
Explicit annotation has the benefit of disabling inference rules and the potential that they lead to a formal isolation that is not preferred. For example, there are circumstances where it is beneficial to guarantee that a closure is `nonisolated` therefore knowing that its execution will hop off the current actor. Explicit annotation also offers the ability to identify a mismatch of intention, such as a case where the developer expected `nonisolated` but inference landed on actor-isolated, and the closure is used in an isolated context. With explicit annotation, the developer would receive a diagnostic about a `nonisolated` closure being used in an actor-isolated context which helpfully identifies this mismatch of intention.
44+
Explicit annotation has the benefit of disabling inference rules and the potential that they lead to a formal isolation that is not preferred. For example, there are circumstances where it is beneficial to guarantee that a closure is `nonisolated` therefore knowing that its execution will hop off the current actor. Explicit annotation also offers the ability to identify a mismatch of intention, such as a case where the developer expected `nonisolated` but inference landed on actor-isolated, and the closure is mistakenly used in an isolated context. Using explicit annotation, the developer would receive a diagnostic about a `nonisolated` closure being used in an actor-isolated context which helpfully identifies this mismatch of intention.
45+
46+
Additionally, there is a difference in how isolation inheritance behaves via the experimental attribute `@_inheritActorContext` (as used by `Task.init`) for isolated parameters vs actor isolation: global actor isolatation is inherited by `Task`'s initializer closure argument, whereas an actor-isolated parameter is not inherited. This makes it challenging to build intuition around how isolation inheritance works. It also makes some kinds of concurrency patterns impossible to use without being overly restrictive.
47+
48+
```swift
49+
class NonSendableType {
50+
@MainActor
51+
func globalActor() {
52+
Task {
53+
// accessing self okay
54+
}
55+
}
56+
57+
func isolatedParameter(_ actor: isolated any Actor) {
58+
Task {
59+
// not okay to access actor
60+
}
61+
}
62+
}
63+
```
3064

3165
## Proposed solution
3266

@@ -50,7 +84,7 @@ actor A {
5084
}
5185
```
5286

53-
Providing a formal replacement of the experimental parameter attribute `@_inheritActorContext` is needed to resolve another area of ambiguity with closure isolation. Its replacement `@inheritsIsolation` changes the behavior so that it unconditionally and implicitly captures the isolation context (as opposed to currently in actor-isolated contexts it being conditional on whether you capture an isolated parameter or isolated capture or actor-isolated function, but guaranteed if the context is isolated to a global actor or `nonisolated`).
87+
Provide a formal replacement of the experimental parameter attribute `@_inheritActorContext` to resolve its ambiguity with closure isolation. Its replacement `@inheritsIsolation` changes the behavior so that it unconditionally and implicitly captures the isolation context (as opposed to currently in actor-isolated contexts it being conditional on whether you capture an isolated parameter or isolated capture or actor-isolated function, but guaranteed if the context is isolated to a global actor or `nonisolated`).
5488

5589
```swift
5690
class Old {
@@ -97,18 +131,64 @@ Opting out of `@inheritsIsolation` can be achieved by explicitly annotating the
97131

98132
The language changes are additive and therefore have no implications on source compatibility. The change to `Task.init` in the standard library does have the potential to isolate some closures that previously were inferred to be `nonisolated`. Prior behavior in those cases could be restored, if desired, by explicitly declaring the closure as `nonisolated`.
99133

134+
It is worth noting that this does not affect the isolation semantics for actor-isolated types that make use of isolated parameters. It is currently impossible to access self in these cases, and even with this new inheritance rule that remains true.
135+
136+
```swift
137+
actor MyActor {
138+
var mutableState = 0
139+
140+
func isolatedParameter(_ actor: isolated any Actor) {
141+
self.mutableState += 1 // invalid
142+
143+
Task {
144+
self.mutableState += 1 // invalid
145+
}
146+
}
147+
}
148+
149+
@MainActor
150+
class MyClass {
151+
var mutableState = 0
152+
153+
func isolatedParameter(_ actor: isolated any Actor) {
154+
self.mutableState += 1 // invalid
155+
156+
Task {
157+
self.mutableState += 1 // invalid
158+
}
159+
}
160+
}
161+
```
162+
100163
## ABI compatibility
101164

102-
The language change does not add or affect ABI since formal isolation is already part of a closure's type regardless of whether it is explicitly specified. The `Task.init` cahnge does not impact ABI since the function is annotated with `@_alwaysEmitIntoClient` and therefore has no ABI.
165+
The language change does not add or affect ABI since formal isolation is already part of a closure's type regardless of whether it is explicitly specified. The `Task.init` change does not impact ABI since the function is annotated with `@_alwaysEmitIntoClient` and therefore has no ABI.
103166

104167
## Implications on adoption
105168

106-
none
169+
This feature can be freely adopted and un-adopted in source code with no deployment constraints and without affecting source or ABI compatibility.
107170

108171
## Alternatives considered
109172

110-
TODO
173+
When this problem was originally brought up, there were several alternatives suggested.
174+
175+
The most obvious is to just not use `Task` in combination with non-Sendable types in this way. Restructuring the code to avoid needing to rely on isolation inheritance in the first place.
176+
177+
```swift
178+
class NonSendableType {
179+
private var internalState = 0
180+
181+
func doSomeStuff(isolatedTo actor: isolated any Actor) async throws {
182+
try await Task.sleep(for: .seconds(1))
183+
print(self.internalState)
184+
}
185+
}
186+
```
187+
188+
Despite this being a useful pattern, it does not address the underlying inheritance semantic differences.
189+
190+
There was also discussion about the ability to make synchronous methods on actors. The scope of such a change is much larger than what is covered here and would still not address the underlying differences.
111191

112-
## Future directions
192+
## Acknowledgments
113193

114-
TODO
194+
Thank you to Franz Busch and Aron Lindberg for looking at the underlying problem so closely and suggesting alternatives. Thank you to Holly Borla for helping to clarify the current behavior, as well as suggesting a path forward that resulted in a much simpler and less-invasive change.

0 commit comments

Comments
 (0)