Closures that have @Sendable function type can only use by-value captures. Captures of immutable values introduced by let are implicitly by-value; any other capture must be specified via a capture list
I wondered why this explicit capture list is needed and wondered whether it would overcomplicate things.
For example, take the following code example:
var searchQuery = "van der"
/// The closure in this example is marked with @Sendable
store.cleanUpContactsInTheBackground { contact in
// Error: Reference to captured var 'searchQuery' in concurrently-executing code
contact.name.contains(searchQuery)
}
The searchQuery property is a value type and benefits from copy-on-write.
My question:
Do we need to capture it by value to create a "snapshot of that time" and to make our code more predictable? As in, since we will actually use the value at a later point, without capturing it we could have a race condition and use a different value than at the time of passing it into cleanUpContactsInTheBackground?
That's the only explanation I could come up with as the closure is expected to cross isolation domains and return at a later point to actually use searchQuery.
apologies if you know this already, but a capture of a mutable local variable like this essentially places the value in a reference-counted 'box' that lives on the heap. this is why changes to the value are 'visible' across different execution contexts (this is nicely explained in this comment/thread).
as such, the issue here is less about 'race conditions' and consistency (though that is also something worth considering), and more about avoiding 'data races' (ah, and it seems you've authored a nice post on the distinction between those terms). the diagnostic prevents capturing these 'by-reference' captures in @Sendable closures because such closures may execute concurrently, and mutations of the capture in such cases could cause data races.
the implementation for this concurrency diagnostic is quite conservative â it is currently based entirely on information present when typechecking, and doesn't use any kind of 'dataflow analysis', like that which powers region based isolation (though commentary suggests some more sophisticated analysis may be an eventual capability). and as a slight aside â one reason i think the diagnostics have to be (perhaps surprisingly) conservative in cases like this is that there's no way to 'tell' the compiler that a closure will be run at most once. so the compiler essentially has to assume a @Sendable closure could be invoked multiple times, concurrently, and apply rules that ensure such cases are still safe.
in your specific example, assuming there are never any writes to the captured value, then i think the code should be data-race free (perhaps by definition), but it will require some source modifications[1] currently to convince the compiler that is the case.
a let or capture list item for immutable values, and Mutex or explicit unsafe opt-outs for mutable cases. âŠī¸
String is a value type, but class Box { var value: String } is not.
When a var is captured into a closure by reference, the semantics is closer to the latter one.