Skip to content

Commit f22ab13

Browse files
committed
Editorial pass
1 parent 6862e5c commit f22ab13

File tree

1 file changed

+37
-10
lines changed

1 file changed

+37
-10
lines changed

proposals/NNNN-global-actor-isolated-types-usability.md

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ This proposal encompasses a collection of changes to concurrency rules concernin
1515

1616
Currently, there exist limitations in the concurrency model around types that are isolated to global actors.
1717

18-
First, let's consider the stored properties of `struct`s isolated to global actors. `let` properties of such types are implicitly treated as `isolated` within the current module if they have `Sendable` type, but `var` properties are not. This poses a number of problems, such as when implementing a protocol conformance. Currently, the only solution is to declare the property `nonisolated(unsafe)`:
18+
First, let's consider the stored properties of `struct`s isolated to global actors. `let` properties of such types are implicitly treated as `nonisolated` within the current module if they have `Sendable` type, but `var` properties are not. This poses a number of problems, such as when implementing a protocol conformance. Currently, the only solution is to declare the property `nonisolated(unsafe)`:
1919

2020
```swift
2121
@MainActor struct S {
@@ -29,13 +29,13 @@ extension S: Equatable {
2929
}
3030
```
3131

32-
However, there is nothing unsafe about treating `x` as nonisolated. The general rule is that concurrency is safe as long as there aren't data races. The type of `x` conforms to `Sendable`, and using a value of `Sendable` type from multiple concurrent contexts shouldn't ever introduce a data race, so any data race involved with an access to `x` would have to be on memory in which `x` is stored. But `x` is part of a value type, which means any access to it is always also an access to the containing `S` value. As long as Swift is properly preventing data races on that larger access, it's always safe to access the `x` part of it. So, first off, there's no reason for Swift to require `(unsafe)` when marking `x` `nonisolated`.
32+
However, there is nothing unsafe about treating `x` as `nonisolated`. The general rule is that concurrency is safe as long as there aren't data races. The type of `x` conforms to `Sendable`, and using a value of `Sendable` type from multiple concurrent contexts shouldn't ever introduce a data race, so any data race involved with an access to `x` would have to be on memory in which `x` is stored. But `x` is part of a value type, which means any access to it is always also an access to the containing `S` value. As long as Swift is properly preventing data races on that larger access, it's always safe to access the `x` part of it. So, first off, there's no reason for Swift to require `(unsafe)` when marking `x` `nonisolated`.
3333

34-
We can do better than that, though. It should be possible to treat a `var` stored property of a global-actor value type as *implicitly* `nonisolated` under the same conditions that a `let` property can be. A stored property from a different module can be changed to a computed property in the future, and those future computed accessors may need to be isolated to the global actor, so allowing access across module boundaries would not be okay for source or binary compatibility. But within the module that defines the property, we know that hasn't happened, so it's fine to use a more relaxed rule.
34+
We can do better than that, though. It should be possible to treat a `var` stored property of a global-actor-isolated value type as *implicitly* `nonisolated` under the same conditions that a `let` property can be. A stored property from a different module can be changed to a computed property in the future, and those future computed accessors may need to be isolated to the global actor, so allowing access across module boundaries would not be okay for source or binary compatibility without an explicit `nonisolated` annotation. But within the module that defines the property, we know that hasn't happened, so it's fine to use a more relaxed rule.
3535

36-
Next, under the current concurrency rules, it is possible for a function type to be both isolated to a global actor and yet not required to be `Sendable`. This is not a useful combination: such a function can only be used if the current context is isolated to the global actor, and in that case the global actor annotation is unnecessary because *all* non-`Sendable` functions will run with global actor isolation.
36+
Next, under the current concurrency rules, it is possible for a function type to be both isolated to a global actor and yet not required to be `Sendable`:
3737

38-
```swift
38+
```swift
3939
func test(globallyIsolated: @escaping @MainActor () -> Void) {
4040
Task {
4141
// error: capture of 'globallyIsolated' with non-sendable type '@MainActor () -> Void' in a `@Sendable` closure
@@ -44,7 +44,7 @@ func test(globallyIsolated: @escaping @MainActor () -> Void) {
4444
}
4545
```
4646

47-
It would be better for a global actor attribute to always imply `@Sendable`.
47+
This is not a useful combination: such a function can only be used if the current context is isolated to the global actor, and in that case the global actor annotation is unnecessary because *all* non-`Sendable` functions will run with global actor isolation. It would be better for a global actor attribute to always imply `@Sendable`.
4848

4949
Because a globally-isolated closure cannot be called concurrently, it's safe for it to capture non-`Sendable` values even if it's implicitly `@Sendable`. Such values just need to be transferred to the global actor's region (if they aren't there already). The same logic also applies to closures that are isolated to a specific actor reference, although it isn't currently possible to write such a closure in a context that isn't isolated to that actor.
5050

@@ -110,7 +110,7 @@ We propose that:
110110
Let's look at the first problem with usability of a `var` property of a main-actor-isolated struct:
111111

112112
```swift
113-
@MainActor
113+
@MainActor
114114
struct S {
115115
var x: Int = 0 // okay ('nonisolated' is inferred within the module)
116116
}
@@ -122,9 +122,25 @@ extension S: Equatable {
122122
}
123123
```
124124

125-
In the above code, `x` is implicitly `nonisolated` within the module. Under this proposal, `nonisolated` is inferred for within the module access of `Sendable` properties of a global-actor-isolated value type. This is data-race safe because the property belongs to a value type, meaning it will be copied every time it crosses an isolation boundary.
125+
In the above code, `x` is implicitly `nonisolated` within the module. Under this proposal, `nonisolated` is inferred for in-module access to `Sendable` properties of a global-actor-isolated value type. A `var` with `Sendable` type within a value type can also have an explicit `nonisolated` modifier to allow synchronous access from outside the module. Once added, `nonisolated` cannot later be removed without potentially breaking clients. The programmer can still convert the property to a computed property, but it has to be a `nonisolated` computed property.
126+
127+
Because `nonisolated` access only applies to stored properties, wrapped properties and `lazy`-initialized properties with `Sendable` type still must be isolated because they are computed properties:
126128

127-
The programmer can still choose to explicitly mark a stored property `nonisolated` to allow synchronous access from outside the module. It is not necessary to use `nonisolated(unsafe)` if the property has `Sendable` type and the property is of a value type. Once added, `nonisolated` cannot later be removed without potentially breaking clients. The programmer can still convert the property to a computed property, but it has to be a `nonisolated` computed property.
129+
```swift
130+
@propertyWrapper
131+
struct MyWrapper<T> { ... }
132+
133+
@MainActor
134+
struct S {
135+
@MyWrapper var x: Int = 0
136+
}
137+
138+
extension S: Equatable {
139+
static nonisolated func ==(lhs: S, rhs: S) -> Bool {
140+
return lhs.x == rhs.x // error
141+
}
142+
}
143+
```
128144

129145
### `@Sendable` inference for global-actor-isolated functions and closures
130146

@@ -162,8 +178,19 @@ func test() {
162178

163179
The above code is data-race safe, since a globally-isolated closure will never operate on the same instance of `NonSendable` concurrently.
164180

165-
Note that under region isolation in SE-0414, capturing a non-`Sendable` value in an actor-isolated closure will transfer the region into the actor, so it is impossible to have concurrent access on non-`Sendable` captures even if the isolated closure is formed outside the actor.
181+
Note that under region isolation in SE-0414, capturing a non-`Sendable` value in an actor-isolated closure will transfer the region into the actor, so it is impossible to have concurrent access on non-`Sendable` captures even if the isolated closure is formed outside the actor:
182+
183+
```swift
184+
class NonSendable {}
166185

186+
func test(ns: NonSendable) async {
187+
let closure { @MainActor in
188+
print(ns) // error: task-isolated value 'ns' can't become isolated to the main actor
189+
}
190+
191+
await closure()
192+
}
193+
```
167194

168195
### Global actor isolation and inheritance
169196

0 commit comments

Comments
 (0)