Skip to content

Commit be2b2ad

Browse files
committed
[Concurrency] Use caller execution for withTaskCannelation handler
The problem is that even with the #isolation parameter the non-Sendable async closure operation _still_ would potentially hop off the caller isolation. We introduced this change because the plan was the non-Sendable closure would run on the isolated parameter's isolation, but that's not actually the case: Instead, we can use the @execution(caller) on the function and closure, in order to guarantee there is no hop between those at all, and developers can trust that adding this cancellation handler will not cause any unexpected isolation changes and hops. The API was always documented to not hop as we execute the operation, so this brings the correct and expected behavior. resolves rdar://140110775 move to nonisolated(nonsending) [Concurrency] latest cancellation handler is emit into client; no ABI entry
1 parent b37d0f6 commit be2b2ad

File tree

4 files changed

+93
-20
lines changed

4 files changed

+93
-20
lines changed

stdlib/public/Concurrency/TaskCancellation.swift

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,10 +67,26 @@ import Swift
6767
/// Therefore, if a cancellation handler must acquire a lock, other code should
6868
/// not cancel tasks or resume continuations while holding that lock.
6969
@available(SwiftStdlib 5.1, *)
70-
#if !$Embedded
71-
@backDeployed(before: SwiftStdlib 6.0)
72-
#endif
70+
@_alwaysEmitIntoClient
71+
nonisolated(nonsending)
7372
public func withTaskCancellationHandler<T>(
73+
operation: nonisolated(nonsending) () async throws -> T,
74+
onCancel handler: @Sendable () -> Void
75+
) async rethrows -> T {
76+
// unconditionally add the cancellation record to the task.
77+
// if the task was already cancelled, it will be executed right away.
78+
let record = unsafe _taskAddCancellationHandler(handler: handler)
79+
defer { unsafe _taskRemoveCancellationHandler(record: record) }
80+
81+
return try await operation()
82+
}
83+
84+
// Note: Deprecated version which would still hop if we did not close over an `isolated` parameter
85+
// with the operation closure. Instead, we should do what the docs of this method promise - and not hop at all,
86+
// by using the new nonisolated(nonsending)
87+
@available(SwiftStdlib 5.1, *)
88+
@_silgen_name("$ss27withTaskCancellationHandler9operation8onCancel9isolationxxyYaKXE_yyYbXEScA_pSgYitYaKlF")
89+
public func _isolatedParam_withTaskCancellationHandler<T>(
7490
operation: () async throws -> T,
7591
onCancel handler: @Sendable () -> Void,
7692
isolation: isolated (any Actor)? = #isolation
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// RUN: %target-run-simple-swift( -target %target-swift-5.1-abi-triple %import-libdispatch) | %FileCheck %s
2+
// REQUIRES: concurrency
3+
// REQUIRES: executable_test
4+
5+
// REQUIRES: concurrency_runtime
6+
// UNSUPPORTED: back_deployment_runtime
7+
// UNSUPPORTED: freestanding
8+
9+
//public func test<T>(_ block: nonisolated(nonsending) () async -> T ) async {
10+
// await block()
11+
//}
12+
13+
actor Canceller {
14+
var hello: String = "checking..."
15+
16+
func testFunc() async {
17+
await withTaskCancellationHandler {
18+
self.assertIsolated("wat in \(#function)!")
19+
print("testFunc.withTaskCancellationHandler") // CHECK: testFunc.withTaskCancellationHandler
20+
self.hello = "done!"
21+
} onCancel: {
22+
// noop
23+
}
24+
25+
// just a simple check to see we executed the closure
26+
27+
await globalTestFunc()
28+
}
29+
}
30+
func globalTestFunc(isolation: isolated (any Actor)? = #isolation) async {
31+
isolation!.assertIsolated("wat in \(#function)!")
32+
await withTaskCancellationHandler {
33+
isolation!.assertIsolated("wat in \(#function)!")
34+
print("globalTestFunc.withTaskCancellationHandler") // CHECK: globalTestFunc.withTaskCancellationHandler
35+
} onCancel: {
36+
// noop
37+
}
38+
}
39+
40+
@MainActor
41+
func testMainActor() async {
42+
MainActor.preconditionIsolated("Expected main actor")
43+
await withTaskCancellationHandler {
44+
MainActor.preconditionIsolated("expected MainActor")
45+
} onCancel: {
46+
// noop
47+
}
48+
}
49+
50+
func testMainActorIsolated(isolation: isolated (any Actor)? = #isolation) async {
51+
isolation!.preconditionIsolated("Expected main actor")
52+
MainActor.preconditionIsolated("Expected main actor")
53+
await withTaskCancellationHandler {
54+
print("_unsafeInheritExecutor_withTaskCancellationHandler")
55+
MainActor.preconditionIsolated("expected MainActor")
56+
} onCancel: {
57+
// noop
58+
}
59+
}
60+
61+
_ = await Canceller().testFunc()
62+
63+
_ = await Task { @MainActor in
64+
await testMainActor()
65+
}.value
66+
67+
_ = await Task { @MainActor in
68+
await testMainActorIsolated()
69+
}.value
70+
71+
print("done") // CHECK: done

test/api-digester/stability-concurrency-abi.test

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,9 @@ Func withCheckedThrowingContinuation(function:_:) has parameter 0 type change fr
8989
Func withCheckedThrowingContinuation(function:_:) has parameter 1 type change from (_Concurrency.CheckedContinuation<τ_0_0, any Swift.Error>) -> () to Swift.String
9090

9191
// #isolation adoption for cancellation handlers; old APIs are kept ABI compatible
92-
Func withTaskCancellationHandler(operation:onCancel:) has been renamed to Func withTaskCancellationHandler(operation:onCancel:isolation:)
93-
Func withTaskCancellationHandler(operation:onCancel:) has mangled name changing from '_Concurrency.withTaskCancellationHandler<A>(operation: () async throws -> A, onCancel: @Sendable () -> ()) async throws -> A' to '_Concurrency.withTaskCancellationHandler<A>(operation: () async throws -> A, onCancel: @Sendable () -> (), isolation: isolated Swift.Optional<Swift.Actor>) async throws -> A'
92+
// but ABI checker does not understand the silgen_names on the ABi-compat APIs
93+
Func withTaskCancellationHandler(operation:onCancel:) has mangled name changing from '_Concurrency.withTaskCancellationHandler<A>(operation: () async throws -> A, onCancel: @Sendable () -> ()) async throws -> A' to '_Concurrency._isolatedParam_withTaskCancellationHandler<A>(operation: () async throws -> A, onCancel: @Sendable () -> (), isolation: isolated Swift.Optional<Swift.Actor>) async throws -> A'
94+
Func withTaskCancellationHandler(operation:onCancel:) has been renamed to Func _isolatedParam_withTaskCancellationHandler(operation:onCancel:isolation:)
9495

9596
// #isolated was adopted and the old methods kept: $ss31withCheckedThrowingContinuation8function_xSS_yScCyxs5Error_pGXEtYaKlF
9697
Func withCheckedContinuation(function:_:) has been renamed to Func withCheckedContinuation(isolation:function:_:)

utils/swift_snapshot_tool/Package.resolved

Lines changed: 0 additions & 15 deletions
This file was deleted.

0 commit comments

Comments
 (0)