Skip to content

Commit cf43771

Browse files
committed
[Educational Notes] Add an explanation for captures in a @Sendable
closure.
1 parent 4353c5a commit cf43771

File tree

2 files changed

+124
-0
lines changed

2 files changed

+124
-0
lines changed

include/swift/AST/EducationalNotes.def

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,12 @@ EDUCATIONAL_NOTES(shared_mutable_state_decl,
9191
"mutable-global-variable.md")
9292
EDUCATIONAL_NOTES(shared_immutable_state_decl,
9393
"mutable-global-variable.md")
94+
EDUCATIONAL_NOTES(non_sendable_capture,
95+
"sendable-closure-captures.md")
96+
EDUCATIONAL_NOTES(concurrent_access_of_local_capture,
97+
"sendable-closure-captures.md")
98+
EDUCATIONAL_NOTES(concurrent_access_of_inout_param,
99+
"sendable-closure-captures.md")
94100

95101
EDUCATIONAL_NOTES(error_in_swift_lang_mode,
96102
"error-in-future-swift-version.md")
Lines changed: 118 additions & 0 deletions
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+
```

0 commit comments

Comments
 (0)