Skip to content

Commit fdd7402

Browse files
committed
[Educational Notes] Add an explanation for sending closure arguments.
1 parent 0f7c991 commit fdd7402

File tree

2 files changed

+69
-0
lines changed

2 files changed

+69
-0
lines changed

include/swift/AST/EducationalNotes.def

+2
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ EDUCATIONAL_NOTES(regionbasedisolation_named_send_yields_race,
8787
"sending-risks-data-race.md")
8888
EDUCATIONAL_NOTES(regionbasedisolation_type_send_yields_race,
8989
"sending-risks-data-race.md")
90+
EDUCATIONAL_NOTES(regionbasedisolation_typed_tns_passed_sending_closure,
91+
"sending-closure-risks-data-race.md")
9092
EDUCATIONAL_NOTES(shared_mutable_state_decl,
9193
"mutable-global-variable.md")
9294
EDUCATIONAL_NOTES(shared_immutable_state_decl,
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)