I'm trying to understand why when capturing a non-sendable value in a Task closure, the data race error goes away. I feel like capturing the value doesn't solve the possible data race, but I'm probably missing something.
class NonSendable {
func stuff() async {}
}
@MainActor
final class Foo {
let value = NonSendable()
func perform() {
Task { [value] in
await value.stuff() // no error
}
Task {
await self.value.stuff() // 🛑 Sending 'self.bar' risks causing data races
}
}
}
hi there, and welcome to the swift forums. it looks to me like you've found a bug, potentially with region based isolation diagnostics. if you wouldn't mind filing a bug report when you have a chance, that would help increase the visibility of the issue.
if we compare the output of the region based isolation logging, it looks like in the case that value.stuff() is called via a capture, the analysis no longer sees that there is a non-sendable, main actor-isolated value involved for some reason, so emits no error. in the case that it's called through the Foo instance variable however, it does and correctly produces a diagnostic.
I actually don't understand why capturing value is unsafe here, could someone explain? It's an async call, so whatever unsafe things may happen within the NonSendable class should be diagnosed within that class, no?
the issue is that if NonSendable.stuff() is allowed to run concurrently, it could introduce data races depending on what it does internally. for example, if we alter the code to something like this:
// run with `swiftc -swift-version 6 -sanitize=thread -Xfrontend -parse-as-library <file>.swift && ./<file>`
class NonSendable {
var state = [Int]()
func stuff() async {
for i in 1...100 {
state.append(i)
}
}
}
@MainActor
final class Foo {
let value = NonSendable()
func perform() async {
let t1 = Task { [value] in
await value.stuff()
}
let t2 = Task { [value] in
await value.stuff()
}
_ = await (t1.value, t2.value)
}
}
@main
enum E {
static func main() async {
await Foo().perform()
}
}
since stuff() is nonisolated and async, the current language semantics mean it will run on the global concurrent executor. as such, there's a race on the internal mutable state, which will typically lead to the program crashing (and TSAN flagging it) at runtime.
the compile-time analysis is supposed to allow passing non-sendable values around somewhat more permissively if they are known to be in 'disconnected' regions. in this particular case though, the value property should presumably always be considered part of the main actor's region, since it is stored state on a @MainActor type.
as an aside – i do wonder if that means value.stuff() should effectively never be callable in this configuration, since i'm not sure how the system could rule out possible re-entrance leading to concurrent execution... but i might be missing something here.
Yes, but shouldn't that be the NonSendable's internal business to detect unsafe usage?
What if stuff() doesn't even touch any properties (in which case it should be declared static of course, and safe to call from anywhere I assume), or if it does, it does it in a safe manner?
To me, making an async call of a function that I don't even know what it does, should be considered safe at the call site.
(Aside: having an async function not tied to an actor is probably a bad idea anyway, I can't think of any scenarios where it would make sense actually.)