3
3
* Proposal: [ SE-0414] ( 0414-region-based-isolation.md )
4
4
* Authors: [ Michael Gottesman] ( https://github.com/gottesmm ) [ Joshua Turcotti] ( https://github.com/jturcotti )
5
5
* Review Manager: [ Holly Borla] ( https://github.com/hborla )
6
- * Status: ** Returned for revision **
6
+ * Status: ** Active Review (January 30 - February 6, 2024) **
7
7
* Implementation: On ` main ` gated behind ` -enable-experimental-feature RegionBasedIsolation `
8
8
* Review: ([ decision notes] ( https://forums.swift.org/t/returned-for-revision-se-0414-region-based-isolation/69123 ) ), ([ review] ( https://forums.swift.org/t/se-0414-region-based-isolation/68805 ) ), ([ pitch 2] ( https://forums.swift.org/t/pitch-region-based-isolation/67888 ) ), ([ pitch 1] ( https://forums.swift.org/t/pitch-safely-sending-non-sendable-values-across-isolation-domains/66566 ) )
9
9
@@ -903,10 +903,12 @@ allowed to be passed over isolation boundaries since they may have captured
903
903
state from within the isolation domain in which the closure is defined. We would
904
904
like to loosen these rules.
905
905
906
- #### Closure Captures
906
+ #### Captures
907
907
908
- The rules for regions defined above specify that a closure's region is the merge
909
- of its non-` Sendable ` captured parameters:
908
+ A non-` Sendable ` closure's region is the merge of its non-` Sendable ` captured
909
+ parameters. As such a nonisolated non-` Sendable ` closure that only captures
910
+ values that are in disconnected regions must itself be in a disconnected region
911
+ and can be transferred:
910
912
911
913
``` swift
912
914
let x = NonSendable ()
@@ -915,61 +917,99 @@ let y = NonSendable()
915
917
// Regions: [(x), (y)]
916
918
let closure = { useValues (x, y) }
917
919
// Regions: [(x, y, closure)]
920
+ await transferToMain (closure) // Ok to transfer!
921
+ // Regions: [{(x, y, closure), @MainActor}]
918
922
```
919
923
920
- Since closure captures are actually a form of parameter when the closure is
921
- invoked, closure parameters like other function parameters are not allowed to be
922
- transferred within a closures body:
924
+ A non-` Sendable ` closure that captures an actor-isolated value is considered to
925
+ be within the actor-isolated region of the value:
923
926
924
927
``` swift
925
- let x = NonSendable ()
926
- // Regions: [(x)]
927
- let closure: () async -> () = {
928
- // Error! Cannot transfer a captured closure parameter!
929
- await transferToMainActor (x)
928
+ actor MyActor {
929
+ var ns = NonSendable ()
930
+
931
+ func doSomething () {
932
+ let closure = { print (self .ns ) }
933
+ // Regions: [{(closure, self.ns), self}]
934
+ await transferToMain (closure) // Error! Cannot transfer value in actor region.
935
+ }
930
936
}
931
937
```
932
938
933
- If a closure is isolated to an actor due to capturing an actor or part of an
934
- actor, then these captured parameters and the closure become merged into the
935
- actor's region meaning that their merged region cannot be transferred :
939
+ When a non- ` Sendable ` value is captured by an actor-isolated non- ` Sendable `
940
+ closure, we treat the value as being transferred into the actor isolation domain
941
+ since the value is now able to merged into actor-isolated state :
936
942
937
943
``` swift
938
- actor Actor {
939
- var ns: NonSendable()
944
+ @MainActor var nonSendableGlobal = NonSendable ()
940
945
941
- func useNonSendable (_ value : NonSendable) { ... }
946
+ func globalActorIsolatedClosureTransfersExample () {
947
+ let x = NonSendable ()
948
+ // Regions: [(x), {(nonSendableGlobal), MainActor}]
949
+ let closure = { @MainActor in
950
+ nonSendableGlobal = x // Error! x is transferred into @MainActor and then accessed later.
951
+ }
952
+ // Regions: [{(nonSendableGlobal, x, closure), MainActor}]
953
+ useValue (x) // Later access is here
954
+ }
942
955
943
- func attemptToTransfer () async {
956
+ actor MyActor {
957
+ var field = NonSendable ()
958
+
959
+ func closureThatCapturesActorIsolatedStateTransfersExample () {
944
960
let x = NonSendable ()
945
- // Regions: [(x), {(self.ns), self}]
946
- let closure: () -> () = { self .useNonSendable (x) }
947
- // Regions: [{(self.ns, x, closure), self}]
948
- await transferToMainActor (closure) // Error! Cannot transfer from actor region
949
- await transferToMainActor (x) // Error! Cannot transfer from actor region
961
+ // Regions: [(x), {(nonSendableGlobal), MainActor}]
962
+ let closure = {
963
+ self .field .doSomething ()
964
+ x.doSomething () // Error! x is transferred into @MainActor and then accessed later.
965
+ }
966
+ // Regions: [{(nonSendableGlobal, x, closure), MainActor}]
967
+ useValue (x) // Later access is here
950
968
}
951
969
}
952
970
```
953
971
954
- In contrast, if a closure is nonisolated and only captures non- ` Sendable ` values
955
- from a disconnected region, then the resulting region from the closures
956
- formation is a disconnected isolation region :
972
+ Importantly this ensures that APIs like ` assumeIsolated ` that take an
973
+ actor-isolated closure argument cannot introduce races by transferring function
974
+ parameters of nonisolated functions into an isolated closure :
957
975
958
976
``` swift
959
- extension Actor {
960
- let x = NonSendable ()
961
- // Regions: [(x)]
962
- let closure: () -> () = { print (x) }
963
- // Regions: [(x, closure)]
964
- // ...
977
+ @MainActor
978
+ final class ContainsNonSendable {
979
+ var ns: NonSendableType = .init ()
980
+
981
+ nonisolated func unsafeSet (_ ns : NonSendableType) {
982
+ self .assumeIsolated { isolatedSelf in
983
+ isolatedSelf.ns = ns // Error! Cannot transfer a parameter!
984
+ }
985
+ }
986
+ }
987
+
988
+ func assumeIsolatedError (actor : ContainsNonSendable) async {
989
+ let x = NonSendableType ()
990
+ actor1.unsafeSet (x)
991
+ useValue (x) // Race is here
992
+ }
993
+ ```
994
+
995
+ Within the body of a non-` Sendable ` closure, the closure and its non-` Sendable `
996
+ captures are treated as being Task isolated since just like a parameter, both
997
+ the closure and the captures may have uses in their caller:
998
+
999
+ ``` swift
1000
+ var x = NonSendable ()
1001
+ var closure = {}
1002
+ closure = {
1003
+ await transferToMain (x) // Error! Cannot transfer Task isolated value!
1004
+ await transferToMain (closure) // Error! Cannot transfer Task isolated value!
965
1005
}
966
1006
```
967
1007
968
- #### Transferring Nonisolated Closures
1008
+ #### Transferring
969
1009
970
- A nonisolated non-` Sendable ` synchronous or asynchronous closure can be
971
- transferred into another isolation domain if the closure's region is never
972
- used again locally:
1010
+ A nonisolated non-` Sendable ` synchronous or asynchronous closure that is in a
1011
+ disconnected region can be transferred into another isolation domain if the
1012
+ closure's region is never used again locally:
973
1013
974
1014
``` swift
975
1015
extension MyActor {
@@ -995,16 +1035,11 @@ extension MyActor {
995
1035
}
996
1036
```
997
1037
998
- This follows from said closure being initialized within a disconnected
999
- isolation region.
1000
-
1001
- #### Isolated Closures
1002
-
1003
- A synchronous non-` Sendable ` closure that is isolated to an actor cannot be
1004
- transferred to a callsite that expects a synchronous closure. This is because as
1005
- part of transferring the closure, we have erased the specific isolation domain
1006
- that the closure was isolated to, so we cannot guarantee that we will invoke the
1007
- value in the actor's isolation domain:
1038
+ An actor-isolated synchronous non-` Sendable ` closure cannot be transferred to a
1039
+ callsite that expects a synchronous closure. This is because as part of
1040
+ transferring the closure, we have erased the specific isolation domain that the
1041
+ closure was isolated to, so we cannot guarantee that we will invoke the value in
1042
+ the actor's isolation domain:
1008
1043
1009
1044
``` swift
1010
1045
@MainActor func transferClosure (_ f : () -> ()) async { ... }
@@ -1023,13 +1058,13 @@ extension Actor {
1023
1058
}
1024
1059
```
1025
1060
1026
- In the future, we may be able to accept this code in the future if we allowed
1027
- for isolated synchronous closures to propagate around the specific isolation
1028
- domain that they belonged to and dynamically swap to it. We discuss * dynamic
1029
- isolation domains * as an extension below.
1061
+ We may be able to accept this code in the future if we allowed for isolated
1062
+ synchronous closures to propagate around the specific isolation domain that they
1063
+ belonged to and dynamically swap to it. We discuss * dynamic isolation domains *
1064
+ as an extension below.
1030
1065
1031
- In contrast, one can pass a synchronous non-` Sendable ` isolated closure
1032
- transferring call site that expects an asynchronous function argument. This is
1066
+ In contrast, one can transfer an actor-isolated synchronous non-` Sendable `
1067
+ closure at a call site that expects an asynchronous function argument. This is
1033
1068
because the closure will be wrapped into an asynchronous thunk that will hop
1034
1069
onto the defining isolation domain of the closure:
1035
1070
@@ -1054,9 +1089,9 @@ In the example above, since the closure is wrapped in the asynchronous thunk and
1054
1089
that thunk hops onto the Actor's executor before calling the closure, we know
1055
1090
that isolation to the actor is preserved when we call the synchronous closure.
1056
1091
1057
- An asynchronous non-Sendable closure that is isolated to an actor can be
1058
- transferred since upon the closure's invocation, we will always hop into the
1059
- actor's isolation domain:
1092
+ An actor-isolated asynchronous non-` Sendable ` closure can be transferred since
1093
+ upon the closure's invocation, we will always hop into the actor's isolation
1094
+ domain:
1060
1095
1061
1096
``` swift
1062
1097
extension Actor {
@@ -1077,6 +1112,119 @@ extension Actor {
1077
1112
}
1078
1113
```
1079
1114
1115
+ #### Closures and Global Actors
1116
+
1117
+ If a closure uses values that are isolated from a global actor in any way, we
1118
+ assume that the closure must also be isolated to that global actor:
1119
+
1120
+ ``` swift
1121
+ @MainActor func mainActorUtility () {}
1122
+
1123
+ @MainActor func mainActorIsolatedClosure () async {
1124
+ let closure = {
1125
+ mainActorUtility ()
1126
+ }
1127
+ // Regions: [{(closure), @MainActor}]
1128
+ await transferToCustomActor (closure) // Error!
1129
+ }
1130
+ ```
1131
+
1132
+ If ` mainActorUtility ` was not called within ` closure ` 's body then ` closure `
1133
+ would be disconnected and could be transferred:"
1134
+
1135
+ ``` swift
1136
+ @MainActor func mainActorUtility () {}
1137
+
1138
+ @MainActor func mainActorIsolatedClosure () async {
1139
+ let closure = {
1140
+ ...
1141
+ }
1142
+ // Regions: [(closure)]
1143
+ await transferToCustomActor (closure) // Ok!
1144
+ }
1145
+ ```
1146
+
1147
+ ### KeyPath
1148
+
1149
+ A non-` Sendable ` keypath that is not actor-isolated is considered to be
1150
+ disconnected and can be transferred into an isolation domain as long as the
1151
+ value's region is not reused again locally:
1152
+
1153
+ ``` swift
1154
+ class Person {
1155
+ var name = " John Smith"
1156
+ }
1157
+
1158
+ class Wrapper <Root : AnyObject > {
1159
+ var root: Root
1160
+ init (root : Root) { self .root = root }
1161
+ func setKeyPath <T >(_ keyPath : ReferenceWritableKeyPath<Root, T>, to value : T) {
1162
+ root[keyPath : keyPath] = value
1163
+ }
1164
+ }
1165
+
1166
+ func useNonIsolatedKeyPath () async {
1167
+ let nonIsolated = Person ()
1168
+ // Regions: [(nonIsolated)]
1169
+ let wrapper = Wrapper (root : nonIsolated)
1170
+ // Regions: [(nonIsolated, wrapper)]
1171
+ let keyPath = \Person.name
1172
+ // Regions: [(nonIsolated, wrapper, keyPath)]
1173
+ await transferToMain (keyPath) // Ok!
1174
+ await wrapper.setKeyPath (keyPath, to : " Jenny Smith" ) // Error!
1175
+ }
1176
+ ```
1177
+
1178
+ A non-` Sendable ` keypath that is actor-isolated is considered to be in the
1179
+ actor's isolation domain and as such cannot be transferred out of the actor's
1180
+ isolation domain:
1181
+
1182
+ ``` swift
1183
+ @MainActor
1184
+ final class MainActorIsolatedKlass {
1185
+ var name = " John Smith"
1186
+ }
1187
+
1188
+ @MainActor
1189
+ func useKeyPath () async {
1190
+ let actorIsolatedKlass = MainActorIsolatedKlass ()
1191
+ // Regions: [{(actorIsolatedKlass.name), @MainActor}]
1192
+ let wrapper = Wrapper (root : actorIsolatedKlass)
1193
+ // Regions: [{(actorIsolatedKlass.name), @MainActor}]
1194
+ let keyPath = \MainActorIsolatedKlass.name
1195
+ // Regions: [{(actorIsolatedKlass.name, keyPath), @MainActor}]
1196
+ await wrapper.setKeyPath (keyPath, to : " value" ) // Error! Cannot pass non-`Sendable`
1197
+ // keypath out of actor isolated domain.
1198
+ }
1199
+ ```
1200
+
1201
+ If a KeyPath captures any values then the KeyPath's region consists of a merge
1202
+ of the captured values regions combined with the actor-isolation region of the
1203
+ KeyPath if the KeyPath is isolated to an actor:
1204
+
1205
+ ``` swift
1206
+ class NonSendableType {
1207
+ subscript <T >(_ t : T) -> Bool { ... }
1208
+ }
1209
+
1210
+ func keyPathInActorIsolatedRegionDueToCapture () async {
1211
+ let mainActorKlass = MainActorIsolatedKlass ()
1212
+ // Regions: [{(mainActorKlass), @MainActor}]
1213
+ let keyPath = \NonSendableType.[mainActorKlass]
1214
+ // Regions: [{(mainActorKlass, keyPath), @MainActor}]
1215
+ await transferToMainActor (keyPath) // Error! Cannot transfer keypath in actor isolated region!
1216
+ }
1217
+
1218
+ func keyPathInDisconnectedRegionDueToCapture () async {
1219
+ let ns = NonSendableType ()
1220
+ // Regions: [(ns)]
1221
+ let keyPath = \NonSendableType.[ns]
1222
+ // Regions: [(ns, keyPath)]
1223
+ await transferToMainActor (ns)
1224
+ useValue (keyPath) // Error! Use of keyPath after transferring ns
1225
+ }
1226
+ ```
1227
+
1080
1228
### Async Let
1081
1229
1082
1230
When an async let binding is initialized with an expression that uses a
0 commit comments