添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

Currently, it is not possible to await an async call within a defer block. Are there reasons that could not be allowed (given an appropriate evolution proposal, etc.)?

Example of what I want to do:

func runOperation() async {
  await disableIdleTimer()
  defer { await enableIdleTimer() }
  // Implement the actual operation here

The idle timer I'm dealing with is MainActor-isolated, but the operation here is designed to run in the background. So I do need to await these calls.

I can, of course, just move the code inside defer {} down to the bottom of the operation. But using a defer makes it easier to see the pairing between the calls, and also ensure that I don't miss it via an early return somewhere in the operation code.

We've been considering adding this feature.

The only concern about this was the implicit suspensions this adds at the end of the function -- but this is the same as an async let so this already exists in the language.

It'd need an evolution proposal and implementation, but it is likely we'd like to do this eventually.

I don't recommend wrapping in Task{} as that dramatically changes semantics and guarantees -- the cleanup will not be guaranteed to complete before the function returns which most certainly is wrong for such enable/disable swapping.

For now you'll have to write the boilerplate at return points, OR write a function like

await withCleanup { await disableIdleTimer() // logic } cleanup: { await enableIdleTimer()

or make your code closure based entirely "withDisabledIdleTimer() { logic }" or similar.

When async defer is brought up people often want cancellation shields as well to write clean up logic that is protected from cancellation. A common example is creating a file descriptor and wanting to close it in an async defer block. This async defer must run otherwise you would leak the descriptor.

Overall, I would love to see both problems solved in the language since we have come across a need for them often.

Speaking of async let, you can in fact leverage it to (hackily) replicate an async defer:

public func deferAsync(_ perform: @escaping @Sendable () async -> Void) async {
    // suspends until cancelled
    for await _ in AsyncStream<Never>.makeStream().stream {}
    await Task { await perform() }.value
func example() async {
    async let _ = deferAsync {
        print("Starting deferred")
        try? await Task.sleep(for: .seconds(1))
        print("Completed deferred")
    print("Starting primary")
    try? await Task.sleep(for: .seconds(1))
    print("Completed primary")
    // == output ==
    // Starting primary
    // <sleeps 1 sec>
    // Completed primary
    // Starting deferred
    // <sleeps 1 sec>
    // Completed deferred

In this snippet, the async let _ is cancelled when example is about to go out of scope, which causes the closure to execute. This does mean the closure is executed in the "cancelled" state so it's necessary to spawn a new unstructured task to "un-cancel" the body. That said this approach has quite a few limitations such as 1) not being able to prove to the compiler that the closure runs at the end instead of in parallel with other code, and 2) not being able to control the order of execution of multiple deferAsync blocks. Indeed, a bona fide defer async would be great to have.

Gonna shamelessly plug my pitch for async deinit, which could achieve the same, at the cost of a wrapper class (Or better yet, a ~Copyable struct).

IMO, structured concurrency is generally better presented using destructors than scopes. Like, suppose if instead of async let, you simply had a StructuredTask which would inherit cancellation when awaited, and cancel and await in its destructor. In addition to doing everything async let does, it would also:

  • Allow returning it from a function, so the caller can await it instead. Currently, this requires a different pattern where you return a Task or closure and the caller async lets it.
  • Allow having it as a member of another class. It cancels and waits when the class deinits
  • Allow adding it to an array. This has the same effect as adding a task to a TaskGroup, but without the with-style scoping. When the Array goes out of scope, the task is cancelled and awaited. An extra parameter to make it not auto-cancel could farther make the same behavior as a TaskGroup (wait all instead of cancel by default). Or just wrapping it in another class that would do it in its deinit
  • But yeah, await inside deinit is something you kind of expect to Just Work™.