简体   繁体   中英

Swift await/async - how to wait synchronously for an async task to complete?

I'm bridging the sync/async worlds in Swift and doing incremental adoption of async/await. I'm trying to invoke an async function that returns a value from a non async function. I understand that explicit use of Task is the way to do that, as described, for instance, here .

The example doesn't really fit as that task doesn't return a value.

After much searching, I haven't been able to find any description of what I'd think was a pretty common ask: synchronous invocation of an asynchronous task (and yes, I understand that that can freeze up the main thread).

What I theoretically would like to write in my synchronous function is this:

let x = Task {
  return await someAsyncFunction()
}.result

However, when I try to do that, I get this compiler error due to trying to access result :

'async' property access in a function that does not support concurrency

One alternative I found was something like:

Task.init {
  self.myResult = await someAsyncFunction()
}

where myResult has to be attributed as a @State member variable.

However, that doesn't work the way I want it to, because there's no guarantee of completing that task prior to Task.init() completing and moving onto the next statement. So how can I wait synchronously for that Task to be complete?

You should not wait synchronously for an async task.

One may come up with a solution similar to this:

func updateDatabase(_ asyncUpdateDatabase: @Sendable @escaping () async -> Void) {
    let semaphore = DispatchSemaphore(value: 0)

    Task {
        await asyncUpdateDatabase()
        semaphore.signal()
    }

    semaphore.wait()
}

Although it works in some simple conditions, according to WWDC 2021 Swift Concurrency: Behind the scenes , this is unsafe. The reason is the system expects you to conform to a runtime contract. The contract requires that

Threads are always able to make forward progress.

That means threads are never blocking. When an asynchronous function reaches a suspension point (eg an await expression), the function can be suspended, but the thread does not block, it can do other works. Based on this contract, the new cooperative thread pool is able to only spawn as many threads as there are CPU cores, avoiding excessive thread context switches. This contract is also the key reason why actors won't cause deadlocks.

The above semaphore pattern violates this contract. The semaphore.wait() function blocks the thread. This can cause problems. For example

func testGroup() {
    Task {
        await withTaskGroup(of: Void.self) { group in
            for _ in 0 ..< 100 {
                group.addTask {
                    syncFunc()
                }
            }
        }
        NSLog("Complete")
    }
}

func syncFunc() {
    let semaphore = DispatchSemaphore(value: 0)
    Task {
        try? await Task.sleep(nanoseconds: 1_000_000_000)
        semaphore.signal()
    }
    semaphore.wait()
}

Here we add 100 concurrent child tasks in the testGroup function, unfortunately the task group will never complete. In my Mac, the system spawns 4 cooperative threads, adding only 4 child tasks is enough to block all 4 threads indefinitely. Because after all 4 threads are blocked by the wait function, there is no more thread available to execute the inner task that signals the semaphore.

Another example of unsafe use is actor deadlock:

func testActor() {
    Task {
        let d = Database()
        await d.updateSettings()
        NSLog("Complete")
    }
}

func updateDatabase(_ asyncUpdateDatabase: @Sendable @escaping () async -> Void) {
    let semaphore = DispatchSemaphore(value: 0)

    Task {
        await asyncUpdateDatabase()
        semaphore.signal()
    }

    semaphore.wait()
}

actor Database {
    func updateSettings() {
        updateDatabase {
            await self.updateUser()
        }
    }

    func updateUser() {

    }
}

Here calling the updateSettings function will deadlock. Because it waits synchronously for the updateUser function, while the updateUser function is isolated to the same actor, so it waits for updateSettings to complete first.

The above two examples use DispatchSemaphore . Using NSCondition in a similar way is unsafe for the same reason. Basically waiting synchronously means blocking the current thread. Avoid this pattern unless you only want a temporary solution and fully understand the risks.

Other than using semaphore, you can wrap your asynchronous task inside an operation like here . You can signal the operation finish once the underlying async task finishes and wait for operation completion using waitUntilFinished() :

let op = TaskOperation {
   try await Task.sleep(nanoseconds: 1_000_000_000)
}
op.waitUntilFinished()

I wrote simple functions that can run asynchronous code as synchronous similar as Kotlin does, you can see code here . It's only for test purposes , though. DO NOT USE IT IN PRODUCTION as async code must be run only asynchronous

Example:

let result = runBlocking {
    try? await Task.sleep(nanoseconds: 1_000_000_000)
    return "Some result"
}
print(result) // prints "Some result"

I've been wondering about this too. How can you start a Task (or several) and wait for them to be done in your main thread, for example? This may be C++ like thinking but there must be a way to do it in Swift as well. For better or worse, I came up with using a global variable to check if the work is done:

import Foundation

var isDone = false

func printIt() async {
    try! await Task.sleep(nanoseconds: 200000000)
    print("hello world")
    isDone = true
}

Task {
    await printIt()
}

while !isDone {
    Thread.sleep(forTimeInterval: 0.1)
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM