|
5 | 5 |
|
6 | 6 | ## Swift 5.10
|
7 | 7 |
|
| 8 | +* Swift 5.10 closes all known static data-race safey holes in complete strict |
| 9 | +concurrency checking. |
| 10 | + |
| 11 | + When writing code against `-strict-concurrency=complete`, Swift 5.10 will |
| 12 | + diagnose all potential for data races at compile time unless an explicit |
| 13 | + unsafe opt out, such as `nonisolated(unsafe)` or `@unchecked Sendable`, is |
| 14 | + used. |
| 15 | + |
| 16 | + For example, in Swift 5.9, the following code crashes at runtime due to a |
| 17 | + `@MainActor`-isolated initializer being evaluated outside the actor, but it |
| 18 | + was not diagnosed under `-strict-concurrency=complete`: |
| 19 | + |
| 20 | + ```swift |
| 21 | + @MainActor |
| 22 | + class MyModel { |
| 23 | + init() { |
| 24 | + MainActor.assertIsolated() |
| 25 | + } |
| 26 | + |
| 27 | + static let shared = MyModel() |
| 28 | + } |
| 29 | + |
| 30 | + func useShared() async { |
| 31 | + let model = MyModel.shared |
| 32 | + } |
| 33 | + |
| 34 | + await useShared() |
| 35 | + ``` |
| 36 | + |
| 37 | + The above code admits data races because a `@MainActor`-isolated static |
| 38 | + variable, which evaluates a `@MainActor`-isolated initial value upon first |
| 39 | + access, is accessed synchronously from a `nonisolated` context. In Swift |
| 40 | + 5.10, compiling the code with `-strict-concurrency=complete` produces a |
| 41 | + warning that the access must be done asynchronously: |
| 42 | + |
| 43 | + ``` |
| 44 | + warning: expression is 'async' but is not marked with 'await' |
| 45 | + let model = MyModel.shared |
| 46 | + ^~~~~~~~~~~~~~ |
| 47 | + await |
| 48 | + ``` |
| 49 | + |
| 50 | + Swift 5.10 fixed numerous other bugs in `Sendable` and actor isolation |
| 51 | + checking to strengthen the guarantees of complete concurrency checking. |
| 52 | + |
| 53 | + Note that the complete concurrency model in Swift 5.10 is conservative. |
| 54 | + Several Swift Evolution proposals are in active development to improve the |
| 55 | + usability of strict concurrency checking ahead of Swift 6. |
| 56 | + |
8 | 57 | * [SE-0412][]:
|
9 | 58 |
|
10 |
| - Under strict concurrency checking, every global or static variable must be either isolated to a global actor or be both immutable and of `Sendable` type. |
| 59 | + Global and static variables are prone to data races because they provide memory that can be accessed from any program context. Strict concurrency checking in Swift 5.10 prevents data races on global and static variables by requiring them to be either: |
| 60 | + |
| 61 | + 1. isolated to a global actor, or |
| 62 | + 2. immutable and of `Sendable` type. |
| 63 | + |
| 64 | + For example: |
11 | 65 |
|
12 | 66 | ```swift
|
13 | 67 | var mutableGlobal = 1
|
14 | 68 | // warning: var 'mutableGlobal' is not concurrency-safe because it is non-isolated global shared mutable state
|
15 | 69 | // (unless it is top-level code which implicitly isolates to @MainActor)
|
16 | 70 |
|
17 |
| - final class NonsendableType { |
18 |
| - init() {} |
| 71 | + @MainActor func mutateGlobalFromMain() { |
| 72 | + mutableGlobal += 1 |
| 73 | + } |
| 74 | + |
| 75 | + nonisolated func mutateGlobalFromNonisolated() async { |
| 76 | + mutableGlobal += 10 |
19 | 77 | }
|
20 | 78 |
|
21 | 79 | struct S {
|
22 |
| - static let immutableNonsendable = NonsendableType() |
23 |
| - // warning: static property 'immutableNonsendable' is not concurrency-safe because it is not either conforming to 'Sendable' or isolated to a global actor |
| 80 | + static let immutableSendable = 10 |
| 81 | + // okay; 'immutableSendable' is safe to access concurrently because it's immutable and 'Int' is 'Sendable' |
24 | 82 | }
|
25 | 83 | ```
|
26 | 84 |
|
27 |
| - The attribute `nonisolated(unsafe)` can be used to annotate a global variable (or any form of storage) to disable static checking of data isolation, but note that without correct implementation of a synchronization mechanism to achieve data isolation, dynamic run-time analysis from exclusivity enforcement or tools such as Thread Sanitizer could still identify failures. |
| 85 | + A new `nonisolated(unsafe)` modifier can be used to annotate a global or static variable to suppress data isolation violations when manual synchronization is provided: |
28 | 86 |
|
29 | 87 | ```swift
|
30 |
| - nonisolated(unsafe) var global: String |
| 88 | + // This global is only set in one part of the program |
| 89 | + nonisolated(unsafe) var global: String! |
31 | 90 | ```
|
32 | 91 |
|
| 92 | + `nonisolated(unsafe)` can be used on any form of storage, including stored properties and local variables, as a more granular opt out for `Sendable` checking, eliminating the need for `@unchecked Sendable` wrapper types in many use cases: |
| 93 | + |
| 94 | + ```swift |
| 95 | + import Dispatch |
| 96 | + |
| 97 | + // 'MutableData' is not 'Sendable' |
| 98 | + class MutableData { ... } |
| 99 | + |
| 100 | + final class MyModel: Sendable { |
| 101 | + private let queue = DispatchQueue(...) |
| 102 | + // 'protectedState' is manually isolated by 'queue' |
| 103 | + nonisolated(unsafe) private var protectedState: MutableData |
| 104 | + } |
| 105 | + ``` |
| 106 | + |
| 107 | + Note that without correct implementation of a synchronization mechanism to achieve data isolation, dynamic run-time analysis from exclusivity enforcement or tools such as the Thread Sanitizer could still identify failures. |
| 108 | + |
33 | 109 | * [SE-0411][]:
|
34 | 110 |
|
35 |
| - Default value expressions can now have the same isolation as the enclosing |
36 |
| - function or the corresponding stored property: |
| 111 | + Swift 5.10 closes a data-race safety hole that previously permitted isolated |
| 112 | + default stored property values to be synchronously evaluated from outside the |
| 113 | + actor. For example, the following code compiles warning-free under |
| 114 | + `-strict-concurrency=complete` in Swift 5.9, but it will crash at runtime at |
| 115 | + the call to `MainActor.assertIsolated()`: |
37 | 116 |
|
38 | 117 | ```swift
|
39 |
| - @MainActor |
40 |
| - func requiresMainActor() -> Int { ... } |
| 118 | + @MainActor func requiresMainActor() -> Int { |
| 119 | + MainActor.assertIsolated() |
| 120 | + return 0 |
| 121 | + } |
41 | 122 |
|
42 |
| - class C { |
43 |
| - @MainActor |
44 |
| - var x: Int = requiresMainActor() |
| 123 | + @MainActor struct S { |
| 124 | + var x = requiresMainActor() |
| 125 | + var y: Int |
| 126 | + } |
| 127 | + |
| 128 | + nonisolated func call() async { |
| 129 | + let s = await S(y: 10) |
45 | 130 | }
|
46 | 131 |
|
47 |
| - @MainActor func defaultArg(value: Int = requiresMainActor()) { ... } |
| 132 | + await call() |
48 | 133 | ```
|
49 | 134 |
|
50 |
| - For isolated default values of stored properties, the implicit initialization |
51 |
| - only happens in the body of an `init` with the same isolation. This closes |
52 |
| - an important data-race safety hole where global-actor-isolated default values |
53 |
| - could inadvertently run synchronously from outside the actor. |
| 135 | + This happens because `requiresMainActor()` is used as a default argument to |
| 136 | + the member-wise initializer of `S`, but default arguments are always |
| 137 | + evaluated in the caller. In this case, the caller runs on the generic |
| 138 | + executor, so the default argument evaluation crashes. |
| 139 | + |
| 140 | + Under `-strict-concurrency=complete` in Swift 5.10, default argument values |
| 141 | + can safely share the same isolation as the enclosing function or stored |
| 142 | + property. The above code is still valid, but the isolated default argument is |
| 143 | + guaranteed to be evaluated in the callee's isolation domain. |
54 | 144 |
|
55 | 145 | ## Swift 5.9.2
|
56 | 146 |
|
|
0 commit comments