Advice regarding Sendable constraints on generic async APIs

Hello,

What does the community wisdom think about async functions that return the result of an input closure? Should the result type be constrained to Sendable, or not?

// No constraint
public func makeValue<T>(
  _ value: @escaping @Sendable () -> T
) async -> T

// Sendable constraint
public func makeValue<T: Sendable>(
  _ value: @escaping @Sendable () -> T
) async -> T

It is assumed that the function can be written without Sendable constraint, which means, AKAIK, that it does not introduce any suspension point before returning the result of its input closure.

At first sight, it looks like the better variant is the one without any constraint. The user will just make the type Sendable if needed by their code:

func run() async {
    let value = await makeValue { ... }
    print(value)           // Sendable not needed, lucky you!
    await actor.use(value) // Sendable needed, deal with it.
}

(I hope I'm correct).

Also, I'm still confused by continuations. When the value is "returned" via an UnsafeContinuation.resume, no suspension point is introduced, and the compiler does not warn if T is not sendable. Can I indeed assume that the value won't cross isolation domains before it ends in the hands of the caller? (if I interpret correctly @John_McCall in [Pitch] Synchronous Mutual Exclusion Lock - #55 by John_McCall)

Finally, there are a lot of pitches and advances about isolation parameters and regions, which aim to ease the use of non-Sendable types.

In the end, I'm on the side of NOT constraining the input type. Are there other considerations that I should think about?

2 Likes

When I read the documentation of UnsafeContinuation.resume(returning:), I understand that the value will cross isolation domains, because it is handled by an executor:

Resume the task that’s awaiting the continuation by returning the given value.

[...]

After calling this method, control immediately returns to the caller. The task continues executing when its executor schedules it.

But then, why doesn't UnsafeContinuation constrain its value to be Sendable :thinking: ? Is it an oversight?

Sorry for my confusion, but it's hard to grok it.

1 Like

Yes it’s a hole in the model. Continuations can slip through not sendable values across async boundaries today.

We’re working on fixing it in Swift 6, “transferring” annotations on parameters is what will be the solution here probably. There may be a tradeoff where the unsafe api maybe remains actually unsafe still or something like that — to be discussed I think.

@Michael_Gottesman is working on those.

4 Likes

Thank you very much :pray: @ktoso

I'll constrain types that are transferred with continuations with Sendable, then.

I was probably wrong: SE-0430 transferring is in review.

@ktoso, would you still say we have a "hole in the model" that should have an impact on our code, since it looks like SE-0430 just confirms the current continuation api without changing anything in the runtime? (cc @beccadax).

I think we wanted the continuation resume APIs to take values transferring, that would address the hole I think. Worth bringing up in th transferring review thread

OK, here it is: SE-0430: `transferring` isolation regions of parameter and result values - #3 by gwendal.roue Don't hesitate adding clarifications if you think I did not ask very clearly :sweat_smile: