I understand that the sending keyword can be used to transfer an object from one actor to another even when it is not Sendable. But the following code doesn't compile:
class NonSendable {}
func getNonSendable() -> sending NonSendable {
return NonSendable()
}
Task { @MainActor in
let ns = await Task {
// error 1: Non-sendable type 'Task<NonSendable, Never>' exiting main actor-isolated context in call to nonisolated property 'value' cannot cross actor boundary
// error 2: Type 'NonSendable' does not conform to the 'Sendable' protocol
return getNonSendable()
}.value
// error 1: Non-sendable type 'NonSendable' in asynchronous access from main actor-isolated context to nonisolated property 'value' cannot cross actor boundary
// error 2: Type 'NonSendable' does not conform to the 'Sendable' protocol
print(ns)
}
Why am I getting these errors and is there a way to create an object inside an actor and move it to another actor?
First, Task's generic signature requires that Success is Sendable, so you can't return non-Sendable values from a Task. It's unfortunate that the compiler doesn't diagnose this first, but
Task {
return getNonSendable()
}
is already enough to produce this error.
Second, because Tasks are unconditionally Sendable themselves, if you return a sending value from a task, it could be copied around after task's completion into other contexts (this is really the reason for the Success: Sendable requirement).
It could be that this is possible to relax and have Task: Sendable where Success: Sendable, Failure: Sendable â then returning non-sendable values would work, but this is not in the language currently.
It might be reasonable to support this via async let so you could do
Task { @MainActor in
async let ns = getNonSendable()
print(await ns)
}
Since async let is scoped and any use of the value must be locally awaited it seems like we'd have the information we need to ensure that we can maintain the disconnected region for the return value?
That's just a simple example, but I was going to use it to perform some heavy work in the background.
That's perfect! Until now I only saw async let in conjunction with an async function, but it seems to work with synchronous functions as well, creating a child task automatically.
async let can be used only in async function, it creates a child task and therefore needs task context, which âexistsâ only for asynchronous functions.
That's right, sorry if I wasn't clear. Of course async let has to be used in an asynchronous function, but what I meant is that it can be used to create a child task even if the called function is not asynchronous.
class NonSendable {}
func getNonSendable() -> sending NonSendable {
return NonSendable()
}
Task { @MainActor in
async let ns = getNonSendable()
print(await ns)
}
Now I fully understand what you meant. Using async let currently doesn't allow to pass a non-Sendable value marked with sending, which is what I had initially thought. In my case, fortunately, I can mark the called function async so that it's still executed off the main thread, and since I don't need to run other code in parallel I can just await the result instead of running it in another Task or with async let. But it would be great if async let and sending would work better together in the future to allow parallel work with sending values.
It turns out I was a little too overenthusiastic about async let. While it works to turn synchronous functions into asynchronous child tasks, often when I see them now I wonder whether the called functions are declared async or not. I'm often tempted to inline the called functions, and if they are async and there's no await in between, it wouldn't change anything, but if they are not async, then dropping async let would suddenly make them synchronous again.