mirror of
https://github.com/apple/swift.git
synced 2025-12-14 20:36:38 +01:00
Merge pull request #79509 from hborla/educational-notes
[Educational Notes] Start adding educational notes for data-race safety.
This commit is contained in:
@@ -33,9 +33,6 @@ EDUCATIONAL_NOTES(non_nominal_extension, "nominal-types.md")
|
||||
EDUCATIONAL_NOTES(associated_type_witness_conform_impossible,
|
||||
"nominal-types.md")
|
||||
|
||||
EDUCATIONAL_NOTES(cannot_infer_closure_result_type,
|
||||
"complex-closure-inference.md")
|
||||
|
||||
EDUCATIONAL_NOTES(invalid_dynamic_callable_type,
|
||||
"dynamic-callable-requirements.md")
|
||||
EDUCATIONAL_NOTES(missing_dynamic_callable_kwargs_method,
|
||||
@@ -86,6 +83,27 @@ EDUCATIONAL_NOTES(result_builder_missing_build_array,
|
||||
EDUCATIONAL_NOTES(multiple_inheritance,
|
||||
"multiple-inheritance.md")
|
||||
|
||||
EDUCATIONAL_NOTES(regionbasedisolation_named_send_yields_race,
|
||||
"sending-risks-data-race.md")
|
||||
EDUCATIONAL_NOTES(regionbasedisolation_type_send_yields_race,
|
||||
"sending-risks-data-race.md")
|
||||
EDUCATIONAL_NOTES(regionbasedisolation_typed_tns_passed_sending_closure,
|
||||
"sending-closure-risks-data-race.md")
|
||||
EDUCATIONAL_NOTES(shared_mutable_state_decl,
|
||||
"mutable-global-variable.md")
|
||||
EDUCATIONAL_NOTES(shared_immutable_state_decl,
|
||||
"mutable-global-variable.md")
|
||||
EDUCATIONAL_NOTES(non_sendable_capture,
|
||||
"sendable-closure-captures.md")
|
||||
EDUCATIONAL_NOTES(concurrent_access_of_local_capture,
|
||||
"sendable-closure-captures.md")
|
||||
EDUCATIONAL_NOTES(concurrent_access_of_inout_param,
|
||||
"sendable-closure-captures.md")
|
||||
EDUCATIONAL_NOTES(actor_isolated_call,
|
||||
"actor-isolated-call.md")
|
||||
EDUCATIONAL_NOTES(actor_isolated_call_decl,
|
||||
"actor-isolated-call.md")
|
||||
|
||||
EDUCATIONAL_NOTES(error_in_swift_lang_mode,
|
||||
"error-in-future-swift-version.md")
|
||||
|
||||
|
||||
46
userdocs/diagnostics/actor-isolated-call.md
Normal file
46
userdocs/diagnostics/actor-isolated-call.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# Calling an actor-isolated method from a synchronous nonisolated context
|
||||
|
||||
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.
|
||||
|
||||
For example:
|
||||
|
||||
```swift
|
||||
@MainActor
|
||||
class MyModel {
|
||||
func update() { ... }
|
||||
}
|
||||
|
||||
func runUpdate(model: MyModel) {
|
||||
model.update()
|
||||
}
|
||||
```
|
||||
|
||||
Building the above code produces an error about calling a main actor isolated method from outside the actor:
|
||||
|
||||
```
|
||||
| func runUpdate(model: MyModel) {
|
||||
| model.update()
|
||||
| `- error: call to main actor-isolated instance method 'update()' in a synchronous nonisolated context
|
||||
| }
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
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:
|
||||
|
||||
```swift
|
||||
@MainActor
|
||||
func runUpdate(model: MyModel) {
|
||||
model.update()
|
||||
}
|
||||
```
|
||||
|
||||
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()`:
|
||||
|
||||
```swift
|
||||
func runUpdate(model: MyModel) {
|
||||
Task { @MainActor in
|
||||
model.update()
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,37 +0,0 @@
|
||||
# Inferring Closure Types
|
||||
If a closure contains a single expression, Swift will consider its body in addition to its signature and the surrounding context when performing type inference. For example, in the following code the type of `doubler` is inferred to be `(Int) -> Int` using only its body:
|
||||
|
||||
```swift
|
||||
let doubler = {
|
||||
$0 * 2
|
||||
}
|
||||
```
|
||||
|
||||
If a closure body is not a single expression, it will not be considered when inferring the closure type. This is consistent with how type inference works in other parts of the language, where it proceeds one statement at a time. For example, in the following code an error will be reported because the type of `evenDoubler` cannot be inferred from its surrounding context and no signature was provided:
|
||||
|
||||
```swift
|
||||
// error: cannot infer return type for closure with multiple statements; add explicit type to disambiguate
|
||||
let evenDoubler = { x in
|
||||
if x.isMultiple(of: 2) {
|
||||
return x * 2
|
||||
} else {
|
||||
return x
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This can be fixed by providing additional contextual information:
|
||||
|
||||
```swift
|
||||
let evenDoubler: (Int) -> Int = { x in
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Or by giving the closure an explicit signature:
|
||||
|
||||
```swift
|
||||
let evenDoubler = { (x: Int) -> Int in
|
||||
// ...
|
||||
}
|
||||
```
|
||||
58
userdocs/diagnostics/mutable-global-variable.md
Normal file
58
userdocs/diagnostics/mutable-global-variable.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# Unsafe mutable global and static variables
|
||||
|
||||
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.
|
||||
|
||||
For example:
|
||||
|
||||
```swift
|
||||
struct Constants {
|
||||
static var value = 10
|
||||
}
|
||||
```
|
||||
|
||||
Building this code with complete concurrency checking will point out the unsafe static variable:
|
||||
|
||||
```
|
||||
| struct Constants {
|
||||
| static var value = 10
|
||||
| |- error: static property 'value' is not concurrency-safe because it is nonisolated global shared mutable state
|
||||
| |- note: convert 'value' to a 'let' constant to make 'Sendable' shared state immutable
|
||||
| |- note: add '@MainActor' to make static property 'value' part of global actor 'MainActor'
|
||||
| `- note: disable concurrency-safety checks if accesses are protected by an external synchronization mechanism
|
||||
```
|
||||
|
||||
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!
|
||||
|
||||
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:
|
||||
|
||||
```swift
|
||||
nonisolated(unsafe) static var value = 10
|
||||
```
|
||||
|
||||
Now consider a static variable with a type that does not conform to `Sendable`:
|
||||
|
||||
```swift
|
||||
class MyModel {
|
||||
static let shared = MyModel()
|
||||
|
||||
// mutable state
|
||||
}
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
```swift
|
||||
class MyModel {
|
||||
@MainActor
|
||||
static let shared = MyModel()
|
||||
}
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
```swift
|
||||
@MainActor
|
||||
class MyModel {
|
||||
static let shared = MyModel()
|
||||
}
|
||||
```
|
||||
118
userdocs/diagnostics/sendable-closure-captures.md
Normal file
118
userdocs/diagnostics/sendable-closure-captures.md
Normal file
@@ -0,0 +1,118 @@
|
||||
# Captures in a `@Sendable` closure
|
||||
|
||||
`@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.
|
||||
|
||||
For example:
|
||||
|
||||
```swift
|
||||
func callConcurrently(
|
||||
_ closure: @escaping @Sendable () -> Void
|
||||
) { ... }
|
||||
|
||||
func capture() {
|
||||
var result = 0
|
||||
result += 1
|
||||
|
||||
callConcurrently {
|
||||
print(result)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The compiler diagnoses the capture of `result` in a `@Sendable` closure:
|
||||
|
||||
```
|
||||
| callConcurrently {
|
||||
| print(result)
|
||||
| `- error: reference to captured var 'result' in concurrently-executing code
|
||||
| }
|
||||
| }
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
```swift
|
||||
func callConcurrently(
|
||||
_ closure: @escaping @Sendable () -> Void
|
||||
) {
|
||||
Task {
|
||||
await withDiscardingTaskGroup { group in
|
||||
for _ in 0..<10 {
|
||||
group.addTask {
|
||||
closure()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
```swift
|
||||
func capture() {
|
||||
var result = 0
|
||||
result += 1
|
||||
|
||||
callConcurrently { [result] in
|
||||
print(result)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This strategy does not apply to captures with non-`Sendable` type. Consider the following example:
|
||||
|
||||
```swift
|
||||
class MyModel {
|
||||
func log() { ... }
|
||||
}
|
||||
|
||||
func capture(model: MyModel) async {
|
||||
callConcurrently {
|
||||
model.log()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The compiler diagnoses the capture of `model` in a `@Sendable` closure:
|
||||
|
||||
```
|
||||
| func capture(model: MyModel) async {
|
||||
| callConcurrently {
|
||||
| model.log()
|
||||
| `- error: capture of 'model' with non-sendable type 'MyModel' in a '@Sendable' closure
|
||||
| }
|
||||
| }
|
||||
```
|
||||
|
||||
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`:
|
||||
|
||||
```swift
|
||||
@MainActor
|
||||
class MyModel {
|
||||
nonisolated func log() { ... }
|
||||
}
|
||||
|
||||
func capture(model: MyModel) async {
|
||||
callConcurrently {
|
||||
model.log()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The compiler will guarantee that the implementation of `log` does not access any main actor state.
|
||||
|
||||
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:
|
||||
|
||||
```swift
|
||||
class MyModel {
|
||||
func log() { ... }
|
||||
}
|
||||
|
||||
func capture(model: MyModel) async {
|
||||
nonisolated(unsafe) let model = model
|
||||
callConcurrently {
|
||||
model.log()
|
||||
}
|
||||
}
|
||||
```
|
||||
67
userdocs/diagnostics/sending-closure-risks-data-race.md
Normal file
67
userdocs/diagnostics/sending-closure-risks-data-race.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# Sending closure risks causing data races
|
||||
|
||||
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.
|
||||
|
||||
For example:
|
||||
|
||||
```swift
|
||||
class MyModel {
|
||||
var count: Int = 0
|
||||
|
||||
func perform() {
|
||||
Task {
|
||||
self.update()
|
||||
}
|
||||
}
|
||||
|
||||
func update() { count += 1 }
|
||||
}
|
||||
```
|
||||
|
||||
The compiler diagnoses the capture of `self` in the task closure:
|
||||
|
||||
```
|
||||
| class MyModel {
|
||||
| func perform() {
|
||||
| Task {
|
||||
| `- error: passing closure as a 'sending' parameter risks causing data races between code in the current task and concurrent execution of the closure
|
||||
| self.update()
|
||||
| `- note: closure captures 'self' which is accessible to code in the current task
|
||||
| }
|
||||
| }
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
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:
|
||||
|
||||
```swift
|
||||
@MainActor
|
||||
class MyModel {
|
||||
func perform() {
|
||||
Task {
|
||||
self.update()
|
||||
}
|
||||
}
|
||||
|
||||
func update() { ... }
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
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:
|
||||
|
||||
```swift
|
||||
class MyModel {
|
||||
static func perform(model: sending MyModel) {
|
||||
Task {
|
||||
model.update()
|
||||
}
|
||||
}
|
||||
|
||||
func update() { ... }
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
52
userdocs/diagnostics/sending-risks-data-race.md
Normal file
52
userdocs/diagnostics/sending-risks-data-race.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# Sending value risks causing data races
|
||||
|
||||
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 `sending 'x' risks causing data races` error indicates that your code can access a non-`Sendable` value from multiple concurrency domains at once.
|
||||
|
||||
For example, if a value can be accessed from the main actor, it's invalid to send the same instance to another concurrency domain while the main actor can still access it. This mistake is common when calling an `async` function on a class from the main actor:
|
||||
|
||||
```swift
|
||||
class Person {
|
||||
var name: String = ""
|
||||
|
||||
func printNameConcurrently() async {
|
||||
print(name)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func onMainActor(person: Person) async {
|
||||
await person.printNameConcurrently()
|
||||
}
|
||||
```
|
||||
|
||||
The above code produces:
|
||||
|
||||
```
|
||||
await person.printNameConcurrently()
|
||||
|- error: sending 'person' risks causing data races
|
||||
`- note: sending main actor-isolated 'person' to nonisolated instance method 'printNameConcurrently()' risks causing data races between nonisolated and main actor-isolated uses
|
||||
```
|
||||
|
||||
This happens because the `printNameConcurrently` function runs off of the main actor, and the `onMainActor` function suspends while waiting for `printNameConcurrently` to complete. While suspended, the main actor can run other tasks that still have access to `person`, which can lead to a data race.
|
||||
|
||||
The most common fix is to change the `async` method to run on the caller's actor using the `@execution(caller)` attribute:
|
||||
|
||||
```swift
|
||||
class Person {
|
||||
var name: String = ""
|
||||
|
||||
@execution(caller)
|
||||
func printNameConcurrently() async {
|
||||
print(name)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func onMainActor(person: Person) async {
|
||||
await person.printNameConcurrently()
|
||||
}
|
||||
```
|
||||
|
||||
This eliminates the risk of data-races because `printNameConcurrently` continues to run on the main actor, so all access to `person` is serialized.
|
||||
|
||||
You can also enable the `AsyncCallerExecution` upcoming feature to make `@execution(caller)` the default for async functions on non-`Sendable` types.
|
||||
Reference in New Issue
Block a user