Swift Await/Async - How to Wait Synchronously for an Async Task to Complete

How to make an async Swift function @synchronized ?

You can have every Task await the prior one. And you can use actor make sure that you are only running one at a time. The trick is, because of actor reentrancy, you have to put that "await prior Task" logic in a synchronous method.

E.g., you can do:

actor Experiment {
private var previousTask: Task<Void, Error>?

func startSomethingAsynchronous() {
previousTask = Task { [previousTask] in
let _ = await previousTask?.result
try await self.doSomethingAsynchronous()
}
}

private func doSomethingAsynchronous() async throws {
let id = OSSignpostID(log: log)
os_signpost(.begin, log: log, name: "Task", signpostID: id, "Start")
try await Task.sleep(nanoseconds: 2 * NSEC_PER_SEC)
os_signpost(.end, log: log, name: "Task", signpostID: id, "End")
}
}

Now I am using os_signpost so I can watch this serial behavior from Xcode Instruments. Anyway, you could start three tasks like so:

import os.log

private let log = OSLog(subsystem: "Experiment", category: .pointsOfInterest)

class ViewController: NSViewController {

let experiment = Experiment()

func startExperiment() {
for _ in 0 ..< 3 {
Task { await experiment.startSomethingAsynchronous() }
}
os_signpost(.event, log: log, name: "Done starting tasks")
}

...
}

And Instruments can visually demonstrate the sequential behavior (where the shows us where the submitting of all the tasks finished), but you can see the sequential execution of the tasks on the timeline:

Sample Image


I actually like to abstract this serial behavior into its own type:

actor SerialTasks<Success> {
private var previousTask: Task<Success, Error>?

func add(block: @Sendable @escaping () async throws -> Success) {
previousTask = Task { [previousTask] in
let _ = await previousTask?.result
return try await block()
}
}
}

And then the asynchronous function for which you need this serial behavior would use the above, e.g.:

class Experiment {
let serialTasks = SerialTasks<Void>()

func startSomethingAsynchronous() async {
await serialTasks.add {
try await self.doSomethingAsynchronous()
}
}

private func doSomethingAsynchronous() async throws {
let id = OSSignpostID(log: log)
os_signpost(.begin, log: log, name: "Task", signpostID: id, "Start")
try await Task.sleep(nanoseconds: 2 * NSEC_PER_SEC)
os_signpost(.end, log: log, name: "Task", signpostID: id, "End")
}
}

How to wait for a bunch of async calls to finish to return a result?

We use task group to perform the requests in parallel and then await the whole group.

The trick, though, is that they will not finish in the same order that we started them. So, if you want to preserve the order of the results, we can return every result as a tuple of the input (the URL) and the output (the string). We then collect the group result into a dictionary, and the map the results back to the original order:

static func save(files: [URL]) async throws -> [String] {
try await withThrowingTaskGroup(of: (URL, String).self) { group in
for file in files {
group.addTask { (file, try await save(file)) }
}

let dictionary = try await group.reduce(into: [:]) { $0[$1.0] = $1.1 }
return files.compactMap { dictionary[$0] }
}
}

There are other techniques to preserve the order of the results, but hopefully this illustrates the basic idea.

Swift - Async/Await not stopping to let task finish

If I understood the problem, you see "Done" being printed before the getToken() method is completed, right?

The problem is that print("Done") is outside of the Task.

When you call Task, it starts running what's in the closure and it immediately resumes after the closure, while your task is running in the background.

Place your print() inside the Task closure, right after the getToken() method, and you'll see that it'll be "Done" after you complete your POST request.

    Task{
let accessToken = await getToken()
print("Done")
}

`Task` blocks main thread when calling async function inside

Consider:

func populate() async {
var items = [String]()
for i in 0 ..< 4_000_000 {
items.append("\(i)")
}
self.items = items
}

You have marked populate with async, but there is nothing asynchronous here, and it will block the calling thread.

Then consider:

func populate() {
Task {
var items = [String]()
for i in 0 ..< 4_000_000 {
items.append("\(i)")
}
self.items = items
}
}

That looks like it must be asynchronous, since it is launching a Task. But this Task runs on the same actor, and because the task is slow and synchronous, it will block the current executor.

If you do not want it to run on the main actor, you can either:

  1. You can leave the view model on the main actor, but manually move the slow synchronous process to a detached task:

    @MainActor
    class ViewModel: ObservableObject {
    @Published var items = [String]()

    func populate() {
    Task.detached {
    var items = [String]()
    for i in 0 ..< .random(in: 4_000_000...5_000_000) { // made it random so I could see values change
    items.append("\(i)")
    }
    await MainActor.run { [items] in
    self.items = items
    }
    }
    }
    }
  2. For the sake of completeness, you could also make the view model to be its own actor, and only designate the relevant observed properties as being on the main actor:

    actor ViewModel: ObservableObject {
    @MainActor
    @Published var items = [String]()

    func populate() {
    Task {
    var items = [String]()
    for i in 0 ..< .random(in: 4_000_000...5_000_000) { // made it random so I could see values change
    items.append("\(i)")
    }
    await MainActor.run { [items] in
    self.items = items
    }
    }
    }
    }

I would generally lean towards the former, but both approaches work.

Either way, it will allow the slow process to not block the main thread. Here I tapped on the button twice:

Sample Image


See WWDC 2021 videos Swift concurrency: Behind the scenes, Protect mutable state with Swift actors, and Swift concurrency: Update a sample app, all of which are useful when trying to grok the transition from GCD to Swift concurrency.



Related Topics



Leave a reply



Submit