SE-0338: Clarify the Execution of Non-Actor-Isolated Async Functions

Hello Swift community,

The review of SE-0338 "Clarify the Execution of Non-Actor-Isolated Async Functions" begins now and runs through January 24, 2022.

Reviews are an important part of the Swift evolution process. All review feedback should be either on this forum thread or, if you would like to keep your feedback private, directly to the review manager. When emailing the review manager directly, please keep the proposal link at the top of the message.

What goes into a review?

The goal of the review process is to improve the proposal under review through constructive criticism and, eventually, determine the direction of Swift. When writing your review, here are some questions you might want to answer in your review:

  • What is your evaluation of the proposal?
  • Is the problem being addressed significant enough to warrant a change to Swift?
  • Does this proposal fit well with the feel and direction of Swift?
  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?
  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

More information about the Swift evolution process is available at

https://github.com/apple/swift-evolution/blob/master/process.md

Thank you,

Doug Gregor
Review Manager

16 Likes

While I do not feel qualified to seriously evaluate this proposal, I do want to state that I am in favor of clarification on this behavior (not surprising, since I ran into this issue in production). Also, a big thank you to everyone who addressed this issue so quickly, especially @kavon!

1 Like

The Sendability section reads:

the arguments and results of all async calls must be Sendable unless … the caller and callee are both known to be non-actor-isolated.

Is that allowed (i.e. known to be safe) because both calls would be part of the same Task?

Yes, that is correct. With a call (whether async or synchronous), the caller and callee are always part of the same task. When both are non-isolated (or both have the same actor isolation), the call won't let the data escape into another actor, either.

Doug

Right. Non-Sendable values require all their uses to be totally ordered by the happens before relation. Tasks totally order their execution, so if a non-Sendable value is isolated to a single task, that's good enough. Actors totally order the execution of their actor-isolated functions, so if a non-Sendable value is isolated to a single actor, that's also good enough. Swift doesn't currently distinguish these things at a precise enough level to allow e.g. an actor function to take a task-isolated parameter; instead, we just assume that all non-Sendable values accessible in an actor-isolated function are meant to be isolated to the actor. As long as that's true, we need to restrict non-Sendable values from crossing an actor isolation boundary on a task; but there's no need to restrict values from being passed around in non-isolated code.

Thanks for the write-up. I'm unclear on 2 points:

  1. Does the non-actor-isolated global executor serialize access (like an actor executor) or allow parallelism?

  2. This has been discussed but it is still unclear to me. Re:the caller and callee are both known to be non-actor-isolated. I understand why that is ok within the same Task, but can you clarify if that non-Sendable args and results can be used from different Tasks like this example:

class Test {
    var int = 0
}

func foo(_ arg: Test) async {
    arg.int = Int.random(in: (0..<100))
}

let test = Test()

Task.detached {
    await foo(test)
}

Task.detached {
    await foo(test)
}
  • What is your evaluation of the proposal?

I think this proposal will do an excellent job of resolving a confusing corner-case of Swift's concurrency model. The existing "sticky" behavior of non-isolated async functions has been the subject of at least one bug report because the reasoning for that behavior wasn't intuitive.

  • Is the problem being addressed significant enough to warrant a change to Swift?

Absolutely. If it were not resolved, I think programmers would keep running into hard-to-debug "overhang" issues.

  • Does this proposal fit well with the feel and direction of Swift?

Yes.

  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?

N/A

  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

I did a quick read and also helped push this issue into the spotlight.

1 Like

No, it does not serialize execution. I should clarify that the non-actor-isolated global executor is not a new concept; it is where new tasks begin running if they do not inherit an actor to be isolated to, as well as where tasks resume running after various kinds of suspension.

No, they cannot. Here's how that restriction works:

  • In general, it's fine to pass a non-Sendable value from a non-isolated async function (like the closure passed to Task.detached to another non-isolated async function (like foo).
  • In this case, since test is a value from an enclosing function, the closure passed to Task.detached must capture it.
  • A closure that captures a non-Sendable value must itself be non-Sendable.
  • But Task.detached must be passed a Sendable function, so this is ill-formed.

This restriction on captures is not new; it's always been part of how Sendable works.

3 Likes

+1 - I agree with everything in the proposal.

  • I agree that async calls from an actor-isolated function should not be guaranteed to run on the actor's executor. Aspects of the actor design (such as reentrancy) suggest to me that they are designed for maximum throughput while allowing safe access to isolated data, not for performing suspending operations on the actor's executor - i.e. it seems to me that the best way to use an actor is to quickly read any state you need up-front, perform any operations with that data on your own time, then return to the actor only to update its state.

  • I agree that all arguments and return values of async functions must be Sendable. I actually had a draft reply to the Improving Sendable pitch which arrived at the same conclusion. Unfortunately I was distracted by other things and forgot to post it (I sometimes write draft replies, then leave them to stew for a bit while I think about things a bit more). Here was my reasoning, which is more about the underlying threads tasks are ultimately scheduled on and types with reference semantics than the more formal reasoning in this proposal:

One thing that I've been wondering about (and perhaps this is the right place to bring it up) is whether we should require that arguments to all async functions are Sendable.

My understanding is that we want Sendable enforcement to eventually become a guarantee against data races. If you pass some data in to an async function, we have no idea whether or not there are other references to its memory, whether it has been passed to any other async functions, and whether any of them will mutate the memory's contents. And when the function you are calling suspends, we don't know which thread it's ultimately going to resume on, and whether or not its reads will be serialised with writes from other functions or overlap with them.

The thing that gives us those guarantees is Sendable.

So yeah, these look like good, important changes to me.

1 Like
Terms of Service

Privacy Policy

Cookie Policy