Marking a class or function as Sendable
ensures it is safe to pass across concurrent boundaries, value types are safe as they implement copy on write, etc. we all find in Swift language documentation. But my point is being Sendable
does not promise that a variable is free of data races. We can have data races even in basic value types such as Int
as demonstrated in code below. So Sendable
does not equate to being data safe from concurrent access/modification, it only means the value is safe to copy across different threads. But my question is what problem does it solve or what is the importance of having Sendable
as a construct then? Can someone please explain it?
var num = 0
DispatchQueue.global(qos: .background).async {
for _ in 0..<100 {
num += 1
}
}
DispatchQueue.global(qos: .background).async {
for _ in 0..<100 {
num -= 1
}
}
As you said, Sendable
allows the compiler to reason about whether a value is safe to send across concurrency domains. This is very useful, because you often write code that sends things across concurrency domains. The compiler can check whether your code is safe or not. Without Sendable
, the compiler will either need to disallow any sending (overly restrictive), or allow all sendings (not safe).
From SE-0302:
Each actor instance and structured concurrency task in a program represents an “island of single threaded-ness”, which makes them a natural synchronization point that holds a bag of mutable state. These perform computation in parallel with other tasks, but we want the vast majority of code in such a system to be synchronization free — building on the logical independence of the actor, and using its mailbox as a synchronization point for its data.
As such, a key question is: “when and how do we allow data to be transferred between concurrency domains?” Such transfers occur in arguments and results of actor method calls and tasks created by structured concurrency, for example.
A very simple example to demonstrate how useful Sendable
is, is:
func f(_ x: SomeType) {
Task {
print(x)
}
}
Is this safe? That depends on whether values of SomeType
are safe to send to the top level Task
. If it is not safe to send, you might end up with the value of SomeType
being shared between the Task
and wherever it originally came from. For example, if SomeType
is a class with mutable properties, this can cause a race:
func g() {
let x = SomeType()
f(x) // the task that f creates might run concurrently with the next line!
x.someProperty = "some new value"
}
If SomeType
is Sendable
, then the compiler can allow the first code snippet to compile.
Here is another example from SE-0302:
actor SomeActor {
// async functions are usable *within* the actor, so this
// is ok to declare.
func doThing(string: NSMutableString) async {}
}
// ... but they cannot be called by other code not protected
// by the actor's mailbox:
func f(a: SomeActor, myString: NSMutableString) async {
// error: 'NSMutableString' may not be passed across actors;
// it does not conform to 'Sendable'
await a.doThing(string: myString)
}
doThing
is isolated to the actor, and the actor could store the string
passed to doThing
in one of its properties. And then you end up with a NSMutableString
being shared between the actor and whoever originally owns it.
If we use String
instead of NSMutableString
, this code is safe because String
is Sendable
. String
is copy-on-write, so the actor modifying it will not affect the whoever owns it originally.