Skip to content

Instantly share code, notes, and snippets.

@nathanhosselton
Last active January 10, 2025 02:03
Show Gist options
  • Save nathanhosselton/98b7851b624eae927a82b924235f5dcd to your computer and use it in GitHub Desktop.
Save nathanhosselton/98b7851b624eae927a82b924235f5dcd to your computer and use it in GitHub Desktop.
@MainActor
protocol NonSendableThing: AnyObject {
func doSomething() async -> Int
}
@MainActor
final class SomeThing: NonSendableThing {
static let sharedExistential: any NonSendableThing = SomeThing()
init() {}
func doSomething() async -> Int {
return 0
}
}
@MainActor
func someOperation() async {
// Why does this error with "sharedExistential cannot cross actor
// boundary"? Shouldn't this be thread safe via its accessor? What does
// it matter that the type isn't Sendable if both its access and all
// of its operations are actor-bound?
async let _ = SomeThing.sharedExistential.doSomething()
// This also errors but instead with "Sending `shared` risks causing
// data races". This is actually a less surprising diagnostic than the
// former, but again, shouldn't this be thread safe since `shared` is
// still actor-bound?
let shared = SomeThing.sharedExistential
async let _ = shared.doSomething()
// This does not error at all, even when using and awaiting the async
// variable. Why not? Isn't this still a non-sendable type crossing an
// actor boundary?
var existentialThing: any NonSendableThing = SomeThing()
async let _ = existentialThing.doSomething()
// Using `SomeThing` directly or declaring `NonSendableThing` to be
// Sendable works as expected since the captured objects are known to
// be Sendable. So I've omitted those examples. This issue isn't a
// blocker in my actual project, I just want to understand what the
// thread safety issue is here, because this seems safe to me.
}
@nathanhosselton
Copy link
Author

nathanhosselton commented Jan 9, 2025

@jamieQ thank you for the proposal ref! from that:

A non-protocol type that is annotated with a global actor implicitly conforms to Sendable.

so I was correct in assuming that being actor-bound implies Sendable conformance (which also answers where SomeThing was getting its implicit Sendable conformance), it's just the specific case of the protocol itself not implicitly getting Sendable conformance, and I guess then, by extension, existentials of that protocol.

feels like this functionality could maybe maybe be added(?), but for my part I am unbothered. it's easy to mark the protocol Sendable and move on. my worry was that I was misunderstanding the implicit sendability of actor-bound types in some fundamental way. which I only arrived at because of this very specific and probably uncommon use case. so thank you so much for taking the time to clear this up given that this was basically self-inflicted, hah.

Swift's concurrency model does not aim to protect you from race conditions, but rather data races.

this callout is also very much appreciated. I conflated the two somewhere along the way. and while I don't understand data races quite as well, now I at least have better framing to start from for understanding Swift concurrency as a whole.

@mattmassicotte
Copy link

Tiny bit of coverage of that here: https://www.swift.org/migration/documentation/swift-6-concurrency-migration-guide/dataracesafety:

More formally, a data race occurs when one thread accesses memory while the same memory is being mutated by another thread.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment