Skip to content

Commit e308605

Browse files
committed
[Concurrency] tasklocal withValue inside actor is safe
1 parent 1f3e159 commit e308605

File tree

5 files changed

+121
-2
lines changed

5 files changed

+121
-2
lines changed

stdlib/public/BackDeployConcurrency/TaskLocal.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,8 +134,9 @@ public final class TaskLocal<Value: Sendable>: Sendable, CustomStringConvertible
134134
///
135135
/// If the value is a reference type, it will be retained for the duration of
136136
/// the operation closure.
137+
@Sendable
137138
@discardableResult
138-
public func withValue<R>(_ valueDuringOperation: Value, operation: () async throws -> R,
139+
public func withValue<R>(_ valueDuringOperation: Value, operation: @Sendable () async throws -> R,
139140
file: String = #file, line: UInt = #line) async rethrows -> R {
140141
// check if we're not trying to bind a value from an illegal context; this may crash
141142
_checkIllegalTaskLocalBindingWithinWithTaskGroup(file: file, line: line)

stdlib/public/Concurrency/TaskLocal.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ public final class TaskLocal<Value: Sendable>: Sendable, CustomStringConvertible
135135
/// If the value is a reference type, it will be retained for the duration of
136136
/// the operation closure.
137137
@discardableResult
138-
public func withValue<R>(_ valueDuringOperation: Value, operation: () async throws -> R,
138+
public func withValue<R>(_ valueDuringOperation: Value, operation: @Sendable () async throws -> R,
139139
file: String = #fileID, line: UInt = #line) async rethrows -> R {
140140
// check if we're not trying to bind a value from an illegal context; this may crash
141141
_checkIllegalTaskLocalBindingWithinWithTaskGroup(file: file, line: line)

test/Concurrency/Runtime/async_task_locals_basic.swift

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,24 @@ func withLocal_body_mustNotEscape() async {
199199
_ = something // silence not used warning
200200
}
201201

202+
@available(SwiftStdlib 5.1, *)
203+
actor Worker {
204+
@TaskLocal
205+
static var declaredInActor: String = "<default-value>"
206+
207+
func setAndRead() async {
208+
print("setAndRead") // CHECK: setAndRead
209+
await Worker.$declaredInActor.withValue("value-1") {
210+
await printTaskLocalAsync(Worker.$declaredInActor) // CHECK-NEXT: TaskLocal<String>(defaultValue: <default-value>) (value-1)
211+
}
212+
}
213+
}
214+
215+
@available(SwiftStdlib 5.1, *)
216+
func inside_actor() async {
217+
await Worker().setAndRead()
218+
}
219+
202220
@available(SwiftStdlib 5.1, *)
203221
@main struct Main {
204222
static func main() async {
@@ -210,5 +228,6 @@ func withLocal_body_mustNotEscape() async {
210228
await nested_3_onlyTopContributes()
211229
await nested_3_onlyTopContributesAsync()
212230
await nested_3_onlyTopContributesMixed()
231+
await inside_actor()
213232
}
214233
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// RUN: %target-run-simple-swift( -Xfrontend -disable-availability-checking -parse-as-library) | %FileCheck %s --dump-input=always
2+
// REQUIRES: executable_test
3+
// REQUIRES: concurrency
4+
// REQUIRES: concurrency_runtime
5+
// UNSUPPORTED: back_deployment_runtime
6+
// UNSUPPORTED: OS=linux-gnu
7+
import Darwin
8+
9+
actor Waiter {
10+
let until: Int
11+
var count: Int
12+
13+
var cc: CheckedContinuation<Int, Never>?
14+
15+
init(until: Int) {
16+
self.until = until
17+
self.count = 0
18+
}
19+
20+
func increment() {
21+
self.count += 1
22+
fputs("> increment (\(self.count)/\(self.until))\n", stderr);
23+
if self.until <= self.count {
24+
if let cc = self.cc {
25+
cc.resume(returning: self.count)
26+
}
27+
}
28+
}
29+
30+
func wait() async -> Int {
31+
if self.until <= self.count {
32+
fputs("> RETURN in Waiter\n", stderr);
33+
return self.count
34+
}
35+
36+
return await withCheckedContinuation { cc in
37+
fputs("> WAIT in Waiter\n", stderr);
38+
self.cc = cc
39+
}
40+
}
41+
}
42+
43+
@available(SwiftStdlib 5.1, *)
44+
func test_taskGroup_void_neverConsume() async {
45+
let until = 100_000_000
46+
let waiter = Waiter(until: until)
47+
48+
let allTasks = await withTaskGroup(of: Void.self, returning: Int.self) { group in
49+
for n in 1...until {
50+
fputs("> enqueue: \(n)\n", stderr);
51+
group.addTask {
52+
fputs("> run: \(n)\n", stderr);
53+
try? await Task.sleep(until: .now + .milliseconds(100), clock: .continuous)
54+
await waiter.increment()
55+
}
56+
}
57+
58+
let void = await group.next()
59+
60+
// wait a little bit, so some tasks complete before we hit the implicit "wait at end of task group scope"
61+
try? await Task.sleep(until: .now + .milliseconds(500), clock: .continuous)
62+
63+
return until
64+
}
65+
66+
// CHECK: all tasks: 100
67+
print("all tasks: \(allTasks)")
68+
print("actor: \(allTasks)")
69+
}
70+
71+
@available(SwiftStdlib 5.1, *)
72+
@main struct Main {
73+
static func main() async {
74+
await test_taskGroup_void_neverConsume()
75+
}
76+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// RUN: %empty-directory(%t)
2+
// RUN: %target-swift-frontend -emit-module -emit-module-path %t/OtherActors.swiftmodule -module-name OtherActors %S/Inputs/OtherActors.swift -disable-availability-checking
3+
// RUN: %target-typecheck-verify-swift -I %t -disable-availability-checking -warn-concurrency -parse-as-library
4+
// REQUIRES: concurrency
5+
6+
// REQUIRES: concurrency
7+
8+
@available(SwiftStdlib 5.1, *)
9+
actor Test {
10+
11+
@TaskLocal static var local: Int?
12+
13+
func run() async {
14+
// This should NOT produce any warnings, the closure withValue uses is @Sendable:
15+
await Test.$local.withValue(42) {
16+
await work()
17+
}
18+
}
19+
20+
func work() async {
21+
print("Hello \(Test.local ?? 0)")
22+
}
23+
}

0 commit comments

Comments
 (0)