Skip to content

Commit cd0708b

Browse files
committed
[Concurrency] task local must not copy null values
1 parent 3ef930c commit cd0708b

File tree

5 files changed

+189
-8
lines changed

5 files changed

+189
-8
lines changed

include/swift/ABI/TaskLocal.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ class TaskLocal {
174174
/// returns, as such, any child tasks potentially accessing the value stack
175175
/// are guaranteed to be completed by the time we pop values off the stack
176176
/// (after the body has completed).
177-
TaskLocal::Item *head;
177+
TaskLocal::Item *head = nullptr;
178178

179179
public:
180180

stdlib/public/Concurrency/TaskLocal.cpp

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -164,11 +164,9 @@ void TaskLocal::Storage::initializeLinkParent(AsyncTask* task,
164164
TaskLocal::Item*
165165
TaskLocal::Item::createParentLink(AsyncTask *task, AsyncTask *parent) {
166166
size_t amountToAllocate = Item::itemSize(/*valueType*/nullptr);
167-
// assert(amountToAllocate % MaximumAlignment == 0); // TODO: do we need this?
168167
void *allocation = _swift_task_alloc_specific(task, amountToAllocate);
169168
Item *item = new(allocation) Item();
170169

171-
// FIXME: parent pointer must to be the parent STORAGE not just the next item.
172170
auto parentHead = parent->_private().Local.head;
173171
if (parentHead) {
174172
if (parentHead->isEmpty()) {
@@ -223,8 +221,14 @@ TaskLocal::Item::createLink(AsyncTask *task,
223221
void TaskLocal::Item::copyTo(AsyncTask *target) {
224222
assert(target && "TaskLocal item attempt to copy to null target task!");
225223

226-
auto item = Item::createLink(target, this->key, this->valueType);
227-
valueType->vw_initializeWithCopy(item->getStoragePtr(), this->getStoragePtr());
224+
// 'parent' pointers are signified by null valueType.
225+
// We must not copy parent pointers, but rather perform a deep copy of all values,
226+
// as such, we skip parent pointers here entirely.
227+
if (!valueType)
228+
return;
229+
230+
auto item = Item::createLink(target, key, valueType);
231+
valueType->vw_initializeWithCopy(item->getStoragePtr(), getStoragePtr());
228232

229233
/// A `copyTo` may ONLY be invoked BEFORE the task is actually scheduled,
230234
/// so right now we can safely copy the value into the task without additional

test/Concurrency/Runtime/async_task_locals_copy_to_async.swift

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ class CustomClass {
9595
}
9696

9797
@available(SwiftStdlib 5.5, *)
98-
func test_async_retains() async {
98+
func test_unstructured_retains() async {
9999
let instance = CustomClass()
100100
CustomClass.$current.withValue(instance) {
101101
print("BEFORE send: \(String(reflecting: CustomClass.current))")
@@ -119,12 +119,43 @@ func test_async_retains() async {
119119
await Task.sleep(2 * 1_000_000_000)
120120
}
121121

122+
@available(SwiftStdlib 5.5, *)
123+
func test_unstructured_noValues() async {
124+
await Task {
125+
// no values to copy
126+
}.value
127+
}
128+
129+
@available(SwiftStdlib 5.5, *)
130+
func downloadImage(from url: String) async throws -> String {
131+
await Task.sleep(10_000)
132+
return ""
133+
}
134+
135+
@available(SwiftStdlib 5.5, *)
136+
func test_unstructured_noValues_childTasks() async {
137+
@Sendable func work() async throws {
138+
let handle = Task {
139+
try await downloadImage(from: "")
140+
}
141+
}
142+
async let one = work()
143+
async let two = work()
144+
async let three = work()
145+
146+
try! await one
147+
try! await two
148+
try! await three
149+
150+
}
151+
122152
@available(SwiftStdlib 5.5, *)
123153
@main struct Main {
124154
static func main() async {
125-
await copyTo_async()
126155
await copyTo_async()
127156
await copyTo_async_noWait()
128-
await test_async_retains()
157+
await test_unstructured_retains()
158+
await test_unstructured_noValues()
159+
await test_unstructured_noValues_childTasks()
129160
}
130161
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// RUN: %target-run-simple-swift(-Xfrontend -enable-experimental-concurrency -parse-as-library %import-libdispatch) | %FileCheck %s
2+
3+
// REQUIRES: executable_test
4+
// REQUIRES: concurrency
5+
// REQUIRES: libdispatch
6+
7+
// rdar://76038845
8+
// UNSUPPORTED: use_os_stdlib
9+
// UNSUPPORTED: back_deployment_runtime
10+
11+
import Dispatch
12+
13+
// For sleep
14+
#if canImport(Darwin)
15+
import Darwin
16+
#elseif canImport(Glibc)
17+
import Glibc
18+
#endif
19+
20+
@available(SwiftStdlib 5.5, *)
21+
enum TL {
22+
@TaskLocal
23+
static var number: Int = 0
24+
@TaskLocal
25+
static var other: Int = 0
26+
}
27+
28+
@available(SwiftStdlib 5.5, *)
29+
@discardableResult
30+
func printTaskLocal<V>(
31+
_ key: TaskLocal<V>,
32+
_ expected: V? = nil,
33+
file: String = #file, line: UInt = #line
34+
) -> V? {
35+
let value = key.get()
36+
print("\(key) (\(value)) at \(file):\(line)")
37+
if let expected = expected {
38+
assert("\(expected)" == "\(value)",
39+
"Expected [\(expected)] but found: \(value), at \(file):\(line)")
40+
}
41+
return expected
42+
}
43+
44+
// ==== ------------------------------------------------------------------------
45+
46+
@available(SwiftStdlib 5.5, *)
47+
func copyTo_sync_noWait() {
48+
print(#function)
49+
TL.$number.withValue(1111) {
50+
TL.$number.withValue(2222) {
51+
TL.$other.withValue(9999) {
52+
Task {
53+
printTaskLocal(TL.$number) // CHECK: TaskLocal<Int>(defaultValue: 0) (2222)
54+
printTaskLocal(TL.$other) // CHECK: TaskLocal<Int>(defaultValue: 0) (9999)
55+
TL.$number.withValue(3333) {
56+
printTaskLocal(TL.$number) // CHECK: TaskLocal<Int>(defaultValue: 0) (3333)
57+
printTaskLocal(TL.$other) // CHECK: TaskLocal<Int>(defaultValue: 0) (9999)
58+
}
59+
}
60+
}
61+
}
62+
}
63+
64+
sleep(1)
65+
}
66+
67+
@available(SwiftStdlib 5.5, *)
68+
func copyTo_sync_noValues() {
69+
Task {
70+
printTaskLocal(TL.$number) // CHECK: TaskLocal<Int>(defaultValue: 0) (0)
71+
}
72+
73+
sleep(1)
74+
}
75+
76+
/// Similar to tests in `async_task_locals_copy_to_async_ but without any task involved at the top level.
77+
@available(SwiftStdlib 5.5, *)
78+
@main struct Main {
79+
static func main() {
80+
copyTo_sync_noWait()
81+
copyTo_sync_noValues()
82+
}
83+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// RUN: %target-run-simple-swift(-Xfrontend -enable-experimental-concurrency -parse-as-library %import-libdispatch) | %FileCheck %s
2+
3+
// REQUIRES: executable_test
4+
// REQUIRES: concurrency
5+
// REQUIRES: libdispatch
6+
7+
// rdar://76038845
8+
// UNSUPPORTED: use_os_stdlib
9+
// UNSUPPORTED: back_deployment_runtime
10+
11+
import _Concurrency
12+
13+
struct MyError: Error {}
14+
struct Image {
15+
}
16+
@available(SwiftStdlib 5.5, *)
17+
func downloadImage(from url: String) async throws -> Image {
18+
await Task.sleep(10_000)
19+
return Image()
20+
}
21+
@available(SwiftStdlib 5.5, *)
22+
actor ImageDownloader {
23+
private enum CacheEntry {
24+
case inProgress(Task<Image, Error>)
25+
case ready(Image)
26+
}
27+
private var cache: [String: CacheEntry] = [:]
28+
func image(from url: String) async throws -> Image? {
29+
if let cached = cache[url] {
30+
switch cached {
31+
case .ready(let image):
32+
return image
33+
case .inProgress(let handle):
34+
return try await handle.value
35+
}
36+
}
37+
let handle = Task {
38+
try await downloadImage(from: url)
39+
}
40+
cache[url] = .inProgress(handle)
41+
do {
42+
let image = try await handle.value
43+
cache[url] = .ready(image)
44+
return image
45+
} catch {
46+
cache[url] = nil
47+
throw error
48+
}
49+
}
50+
}
51+
@available(SwiftStdlib 5.5, *)
52+
@main struct Main {
53+
static func main() async {
54+
let downloader = ImageDownloader()
55+
async let image1 = downloader.image(from: "https://homepages.cae.wisc.edu/~ece533/images/airplane.png")
56+
async let image2 = downloader.image(from: "https://homepages.cae.wisc.edu/~ece533/images/airplane.png")
57+
async let image3 = downloader.image(from: "https://homepages.cae.wisc.edu/~ece533/images/airplane.png")
58+
print("\(try! await image1!)")
59+
print("\(try! await image2!)")
60+
print("\(try! await image3!)")
61+
print("okey") // CHECK: OK
62+
}
63+
}

0 commit comments

Comments
 (0)