Skip to content

Commit 3a744d2

Browse files
authored
Merge pull request #79509 from hborla/educational-notes
[Educational Notes] Start adding educational notes for data-race safety.
2 parents edc7420 + fdd7402 commit 3a744d2

7 files changed

+362
-40
lines changed

include/swift/AST/EducationalNotes.def

+21-3
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,6 @@ EDUCATIONAL_NOTES(non_nominal_extension, "nominal-types.md")
3333
EDUCATIONAL_NOTES(associated_type_witness_conform_impossible,
3434
"nominal-types.md")
3535

36-
EDUCATIONAL_NOTES(cannot_infer_closure_result_type,
37-
"complex-closure-inference.md")
38-
3936
EDUCATIONAL_NOTES(invalid_dynamic_callable_type,
4037
"dynamic-callable-requirements.md")
4138
EDUCATIONAL_NOTES(missing_dynamic_callable_kwargs_method,
@@ -86,6 +83,27 @@ EDUCATIONAL_NOTES(result_builder_missing_build_array,
8683
EDUCATIONAL_NOTES(multiple_inheritance,
8784
"multiple-inheritance.md")
8885

86+
EDUCATIONAL_NOTES(regionbasedisolation_named_send_yields_race,
87+
"sending-risks-data-race.md")
88+
EDUCATIONAL_NOTES(regionbasedisolation_type_send_yields_race,
89+
"sending-risks-data-race.md")
90+
EDUCATIONAL_NOTES(regionbasedisolation_typed_tns_passed_sending_closure,
91+
"sending-closure-risks-data-race.md")
92+
EDUCATIONAL_NOTES(shared_mutable_state_decl,
93+
"mutable-global-variable.md")
94+
EDUCATIONAL_NOTES(shared_immutable_state_decl,
95+
"mutable-global-variable.md")
96+
EDUCATIONAL_NOTES(non_sendable_capture,
97+
"sendable-closure-captures.md")
98+
EDUCATIONAL_NOTES(concurrent_access_of_local_capture,
99+
"sendable-closure-captures.md")
100+
EDUCATIONAL_NOTES(concurrent_access_of_inout_param,
101+
"sendable-closure-captures.md")
102+
EDUCATIONAL_NOTES(actor_isolated_call,
103+
"actor-isolated-call.md")
104+
EDUCATIONAL_NOTES(actor_isolated_call_decl,
105+
"actor-isolated-call.md")
106+
89107
EDUCATIONAL_NOTES(error_in_swift_lang_mode,
90108
"error-in-future-swift-version.md")
91109

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Calling an actor-isolated method from a synchronous nonisolated context
2+
3+
Calls to actor-isolated methods from outside the actor must be done asynchronously. Otherwise, access to actor state can happen concurrently and lead to data races. These rules also apply to global actors like the main actor.
4+
5+
For example:
6+
7+
```swift
8+
@MainActor
9+
class MyModel {
10+
func update() { ... }
11+
}
12+
13+
func runUpdate(model: MyModel) {
14+
model.update()
15+
}
16+
```
17+
18+
Building the above code produces an error about calling a main actor isolated method from outside the actor:
19+
20+
```
21+
| func runUpdate(model: MyModel) {
22+
| model.update()
23+
| `- error: call to main actor-isolated instance method 'update()' in a synchronous nonisolated context
24+
| }
25+
```
26+
27+
The `runUpdate` function doesn't specify any actor isolation, so it is `nonisolated` by default. `nonisolated` methods can be called from any concurrency domain. To prevent data races, `nonisolated` methods cannot access actor isolated state in their implementation. If `runUpdate` is called from off the main actor, calling `model.update()` could mutate main actor state at the same time as another task running on the main actor.
28+
29+
To resolve the error, `runUpdate` has to make sure the call to `model.update()` is on the main actor. One way to do that is to add main actor isolation to the `runUpdate` function:
30+
31+
```swift
32+
@MainActor
33+
func runUpdate(model: MyModel) {
34+
model.update()
35+
}
36+
```
37+
38+
Alternatively, if the `runUpdate` function is meant to be called from arbitrary concurrent contexts, create a task isolated to the main actor to call `model.update()`:
39+
40+
```swift
41+
func runUpdate(model: MyModel) {
42+
Task { @MainActor in
43+
model.update()
44+
}
45+
}
46+
```

userdocs/diagnostics/complex-closure-inference.md

-37
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Unsafe mutable global and static variables
2+
3+
Concurrency checking prohibits mutable global and static variables that are `nonisolated` because they can be accessed from arbitrary concurrency domains at once and lead to data races.
4+
5+
For example:
6+
7+
```swift
8+
struct Constants {
9+
static var value = 10
10+
}
11+
```
12+
13+
Building this code with complete concurrency checking will point out the unsafe static variable:
14+
15+
```
16+
| struct Constants {
17+
| static var value = 10
18+
| |- error: static property 'value' is not concurrency-safe because it is nonisolated global shared mutable state
19+
| |- note: convert 'value' to a 'let' constant to make 'Sendable' shared state immutable
20+
| |- note: add '@MainActor' to make static property 'value' part of global actor 'MainActor'
21+
| `- note: disable concurrency-safety checks if accesses are protected by an external synchronization mechanism
22+
```
23+
24+
If the type of the variable conforms to `Sendable` and the value is never changed, a common fix is to change the `var` to a `let` to make the state immutable. Immutable state is safe to access concurrently!
25+
26+
If you carefully access the global variable in a way that cannot cause data races, such as by wrapping all accesses in an external synchronization mechanism like a lock or a dispatch queue, you can apply `nonisolated(unsafe)` to opt out of concurrency checking:
27+
28+
```swift
29+
nonisolated(unsafe) static var value = 10
30+
```
31+
32+
Now consider a static variable with a type that does not conform to `Sendable`:
33+
34+
```swift
35+
class MyModel {
36+
static let shared = MyModel()
37+
38+
// mutable state
39+
}
40+
```
41+
42+
This code is also diagnosed under complete concurrency checking. Even though the `shared` variable is a `let` constant, the `MyModel` type is not `Sendable`, so it could have mutable stored properties. A common fix in this case is to isolate the variable to the main actor:
43+
44+
```swift
45+
class MyModel {
46+
@MainActor
47+
static let shared = MyModel()
48+
}
49+
```
50+
51+
Alternatively, isolate the `MyModel` class to the main actor, which will also make the type `Sendable` because the main actor protects access to all mutable state:
52+
53+
```swift
54+
@MainActor
55+
class MyModel {
56+
static let shared = MyModel()
57+
}
58+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
# Captures in a `@Sendable` closure
2+
3+
`@Sendable` closures can be called multiple times concurrently, so any captured values must also be safe to access concurrently. To prevent data races, the compiler prevents capturing mutable values in a `@Sendable` closure.
4+
5+
For example:
6+
7+
```swift
8+
func callConcurrently(
9+
_ closure: @escaping @Sendable () -> Void
10+
) { ... }
11+
12+
func capture() {
13+
var result = 0
14+
result += 1
15+
16+
callConcurrently {
17+
print(result)
18+
}
19+
}
20+
```
21+
22+
The compiler diagnoses the capture of `result` in a `@Sendable` closure:
23+
24+
```
25+
| callConcurrently {
26+
| print(result)
27+
| `- error: reference to captured var 'result' in concurrently-executing code
28+
| }
29+
| }
30+
```
31+
32+
Because the closure is marked `@Sendable`, the implementation of `callConcurrently` can call `closure` multiple times concurrently. For example, multiple child tasks within a task group can call `closure` concurrently:
33+
34+
```swift
35+
func callConcurrently(
36+
_ closure: @escaping @Sendable () -> Void
37+
) {
38+
Task {
39+
await withDiscardingTaskGroup { group in
40+
for _ in 0..<10 {
41+
group.addTask {
42+
closure()
43+
}
44+
}
45+
}
46+
}
47+
}
48+
```
49+
50+
If the type of the capture is `Sendable` and the closure only needs the value of the variable at the point of capture, resolve the error by explicitly capturing the variable by value in the closure's capture list:
51+
52+
```swift
53+
func capture() {
54+
var result = 0
55+
result += 1
56+
57+
callConcurrently { [result] in
58+
print(result)
59+
}
60+
}
61+
```
62+
63+
This strategy does not apply to captures with non-`Sendable` type. Consider the following example:
64+
65+
```swift
66+
class MyModel {
67+
func log() { ... }
68+
}
69+
70+
func capture(model: MyModel) async {
71+
callConcurrently {
72+
model.log()
73+
}
74+
}
75+
```
76+
77+
The compiler diagnoses the capture of `model` in a `@Sendable` closure:
78+
79+
```
80+
| func capture(model: MyModel) async {
81+
| callConcurrently {
82+
| model.log()
83+
| `- error: capture of 'model' with non-sendable type 'MyModel' in a '@Sendable' closure
84+
| }
85+
| }
86+
```
87+
88+
If a type with mutable state can be referenced concurrently, but all access to mutable state happens on the main actor, isolate the type to the main actor and mark the methods that don't access mutable state as `nonisolated`:
89+
90+
```swift
91+
@MainActor
92+
class MyModel {
93+
nonisolated func log() { ... }
94+
}
95+
96+
func capture(model: MyModel) async {
97+
callConcurrently {
98+
model.log()
99+
}
100+
}
101+
```
102+
103+
The compiler will guarantee that the implementation of `log` does not access any main actor state.
104+
105+
If you manually ensure data-race safety, such as by using an external synchronization mechanism, you can use `nonisolated(unsafe)` to opt out of concurrency checking:
106+
107+
```swift
108+
class MyModel {
109+
func log() { ... }
110+
}
111+
112+
func capture(model: MyModel) async {
113+
nonisolated(unsafe) let model = model
114+
callConcurrently {
115+
model.log()
116+
}
117+
}
118+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# Sending closure risks causing data races
2+
3+
If a type does not conform to `Sendable`, the compiler enforces that each instance of that type is only accessed by one concurrency domain at a time. The compiler also prevents you from capturing values in closures that are sent to another concurrency domain if the value can be accessed from the original concurrency domain too.
4+
5+
For example:
6+
7+
```swift
8+
class MyModel {
9+
var count: Int = 0
10+
11+
func perform() {
12+
Task {
13+
self.update()
14+
}
15+
}
16+
17+
func update() { count += 1 }
18+
}
19+
```
20+
21+
The compiler diagnoses the capture of `self` in the task closure:
22+
23+
```
24+
| class MyModel {
25+
| func perform() {
26+
| Task {
27+
| `- error: passing closure as a 'sending' parameter risks causing data races between code in the current task and concurrent execution of the closure
28+
| self.update()
29+
| `- note: closure captures 'self' which is accessible to code in the current task
30+
| }
31+
| }
32+
```
33+
34+
This code is invalid because the task that calls `perform()` runs concurrently with the task that calls `update()`. The `MyModel` type does not conform to `Sendable`, and it has unprotected mutable state that both concurrent tasks could access simultaneously.
35+
36+
To eliminate the risk of data races, all tasks that can access the `MyModel` instance must be serialized. The easiest way to accomplish this is to isolate `MyModel` to a global actor, such as the main actor:
37+
38+
```swift
39+
@MainActor
40+
class MyModel {
41+
func perform() {
42+
Task {
43+
self.update()
44+
}
45+
}
46+
47+
func update() { ... }
48+
}
49+
```
50+
51+
This resolves the data race because the two tasks that can access the `MyModel` value must switch to the main actor to access its state and methods.
52+
53+
The other approach to resolving the error is to ensure that only one task has access to the `MyModel` value at a time. For example:
54+
55+
```swift
56+
class MyModel {
57+
static func perform(model: sending MyModel) {
58+
Task {
59+
model.update()
60+
}
61+
}
62+
63+
func update() { ... }
64+
}
65+
```
66+
67+
This code is safe from data races because the caller of `perform` cannot access the `model` parameter again after the call. The `sending` parameter modifier indicates that the implementation of the function sends the value to a different concurrency domain, so it's no longer safe to access the value in the caller. This ensures that only one task has access to the value at a time.

0 commit comments

Comments
 (0)