Skip to content

Commit 1a224bf

Browse files
committed
[Educational Notes] Add an explanation for sending closure arguments.
1 parent ad50eb3 commit 1a224bf

File tree

2 files changed

+67
-0
lines changed

2 files changed

+67
-0
lines changed

include/swift/AST/EducationalNotes.def

Lines changed: 2 additions & 0 deletions
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,
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# Sending closure risks causing data races
2+
3+
If a type does not conform to `Sendable` the compiler will enforce that each instance of that type is only accessed by one concurrency domain at a time. The compiler will also prevent 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+
func perform() {
10+
Task {
11+
self.update()
12+
}
13+
}
14+
15+
func update() { ... }
16+
}
17+
```
18+
19+
Building with complete concurrency checking will diagnose the capture of `self` in the task closure:
20+
21+
```
22+
| class MyModel {
23+
| func perform() {
24+
| Task {
25+
| `- error: passing closure as a 'sending' parameter risks causing data races between code in the current task and concurrent execution of the closure
26+
| self.update()
27+
| `- note: closure captures 'self' which is accessible to code in the current task
28+
| }
29+
| }
30+
```
31+
32+
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`, so this means the concurrent tasks can access mutable state simultaneously.
33+
34+
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:
35+
36+
```swift
37+
@MainActor
38+
class MyModel {
39+
func perform() {
40+
Task {
41+
self.update()
42+
}
43+
}
44+
45+
func update() { ... }
46+
}
47+
```
48+
49+
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.
50+
51+
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:
52+
53+
```swift
54+
class MyModel {
55+
static func perform(model: sending MyModel) {
56+
Task {
57+
model.update()
58+
}
59+
}
60+
61+
func update() { ... }
62+
}
63+
```
64+
65+
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 will send the value to a difference 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)