Skip to content

Commit d276444

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

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. The compiler will prevent mutable captures in a `@Sendable` closure under complete concurrency checking.
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+
Building with complete concurrency checking will diagnose 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 closure is `@Sendable`, the implementation of `callConcurrently` is allowed to call `closure` multiple times concurrently, e.g. using a task group:
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, you can resolve the error using capture by value in a 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+
Building with complete concurrency checking will diagnose 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, the best way to model that is isolating the type to a global actor and marking the methods that don't access mutable state with `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)