Hello! I have another question about concurrency isolation checks that I would like to clarify.
Given a non-isolated type as follows:
class File {
func read() -> String { "" }
}
That is part of an old library not ported yet to Swift 6. So it's used in our codebase with a @preconcurrency import
.
When trying to use task groups to spawn a child task for each individual file in an array we get a sendaiblity error:
func someFunction(
files: [File]
) async throws {
await withTaskGroup(of: Void.self) { group in
for file in files {
group.addTask { // Task-isolated value of type '() async -> Void' passed as a strongly transferred parameter; later accesses could race
let c = file.read()
print(c)
}
}
}
}
I understand that file
is not senadble, but since is not used after being captured by addTask
I would assume this is safe?
Our existing workaround is to create a closure where we can use sending
to give more information to the compiler:
func someFunctionWorkaround(
files: [File]
) async throws {
let workaroundAddTask: (
_ file: sending File, // NOTE FILE IS SENDING
_ group: inout TaskGroup<Void>
) -> Void = { file, group in
group.addTask {
let c = file.read()
print(c)
}
}
await withTaskGroup(of: Void.self) { group in
for file in files {
workaroundAddTask(file, &group) // COMPILES
}
}
}
With this workaround we get a successful compilation. But should it be necessary?
After further investigaiton I have more doubts. If instead of having the non-senadble File
class in another module that is imported via preconcurrency, I have it on the same module, then the first example fails with the same errors but now the workaround doesn't work.
// non-sendable, but now in the same module
class File {
func read() -> String { "" }
}
func someFunction(
files: [File]
) async throws {
await withTaskGroup(of: Void.self) { group in
for file in files {
group.addTask { // same error as before
let c = file.read()
print(c)
}
}
}
}
func someFunctionWorkaround(
files: [File]
) async throws {
let workaroundAddTask: (
_ file: sending File,
_ group: inout TaskGroup<Void>
) -> Void = { file, group in
group.addTask {
let c = file.read()
print(c)
}
}
await withTaskGroup(of: Void.self) { group in
for file in files {
workaroundAddTask(file, &group) // but now this errors too!
// Sending 'file' risks causing data races
// Task-isolated 'file' is passed as a 'sending' parameter; Uses
// in callee may race with later task-isolated uses
}
}
}
I imagine this is due to the difference with @preconcurrency
but it was surprising. The new error also seems a bit weird since there are no uses after is sent. (at least from my code, hence my question... I thought maybe the for loop could have something to do so I changed to iterating indices but the issue is the same)
- How do arrays and its elements play a role in region isolation? Is passing an element of an array actually try to transfer the entire array to the new region?
- If so, is there any workaround?
- I assume the difference on behavior with
@preconcurrency
is expected?
Tried with Xcode Version 16.1 beta 3 (16B5029d)
swift-driver version: 1.115 Apple Swift version 6.0.2 (swiftlang-6.0.2.1.2 clang-1600.0.26.4)
EDIT: Repo with the code above in a SPM setup with modules to easy replicate this.