Swift Execute Asynchronous Tasks in Order

Need help to understand the order of execution in conjunction with async and await

You ask:

So why does Message1 not appear right after step 1 as coded?

Because you are blocking the main thread with your repeat-while loop. Your code is a perfect demonstration of why you should never block the main thread, as the UI cannot be updated until you free up the main thread. Any system events will be prevented, too. If you block the main thread for long enough, you even risk having your app unceremoniously killed by the watchdog process (designated with the termination code 0x8badf00d, pronounced “ate bad food”).

Your code says:

// The intention of the next lines is to hold the
// processing of the main thread until the async task is
// completed.

That is the problem. That is why your UI has frozen. This is to be categorically avoided.

FYI, this behavior is not unique to Swift Concurrency. This issue would manifest itself if you had attempted to block the main thread waiting for something running slowly on a GCD background queue, too.


BTW, I noticed that testAsync is calling sleep. But, as Apple says in Swift concurrency: Behind the scenes:

Recall that with Swift, the language allows us to uphold a runtime contract that threads will always be able to make forward progress. It is based on this contract that we have built a cooperative thread pool to be the default executor for Swift. As you adopt Swift concurrency, it is important to ensure that you continue to maintain this contract in your code as well so that the cooperative thread pool can function optimally.

Thus you should not call sleep inside testAsync. You can use Task.sleep(nanoseconds:), though:

func testAsync() async throws {
print("in testAsync step 2")
try await Task.sleep(nanoseconds: NSEC_PER_SEC * 2)
print("in testAsync step 3")
testAsyncDone = true
}

It looks like a traditional “sleep” API, but as the docs say, this “does not block the underlying thread” (emphasis added).


Also, note that this code is not thread-safe. You are accessing testasyncDone from multiple threads without any synchronization. Turn on the Thread Sanitizer (TSAN), and it will report:

Sample Image

You can synchronize this yourself (locks or GCD are traditional mechanisms), or with the new Swift Concurrency system, we would use an actor. See WWDC 2021 video, Protect mutable state with Swift actors.


So, I suspect that this is going to trigger the response, “well, if I can’t block the main thread, then what should I do?”

Let us consider a bunch of alternatives.

  1. If you really need two tasks running in parallel and coordinate this with some state variable, one method counting until the other changes the state of that variable, you could first create an actor to capture this state:

    actor TestAsyncState {
    private var _isDone = false

    func finish() {
    _isDone = true
    }

    func isDone() -> Bool {
    _isDone
    }
    }

    Then you could check this actor state:

    var testAsyncState = TestAsyncState()

    @IBAction func didTapButton(_ sender: UIButton) {
    print("MyButton Pressed step 1")

    Task.detached { [self] in
    await MainActor.run { message1.text = "In Button action - start" }
    try await self.testAsync()
    print("MyButton Pressed step 4")
    await testAsyncState.finish()
    }

    Task.detached { [self] in
    // The intention of the next lines is to keep ticking
    // until the state actor isDone or we reach 100 iterations

    var count = 0
    repeat {
    count += 1
    try await Task.sleep(nanoseconds: NSEC_PER_SEC / 10)
    print(count)
    } while await !testAsyncState.isDone() && count < 100

    await MainActor.run { message2.text = "In Button action - finished" }
    }
    }
  2. Or, alternatively, you could bypass this actor state variable entirely, and just cancel the counting task when the other finishes:

    @IBAction func didTapButton(_ sender: UIButton) {
    print("MyButton Pressed step 1")

    let tickingTask = Task.detached { [self] in
    // The intention of the next lines is to keep ticking
    // until this is canceled or we reach 100 iterations

    do {
    var count = 0
    repeat {
    count += 1
    try await Task.sleep(nanoseconds: NSEC_PER_SEC / 10)
    print(count)
    } while !Task.isCancelled && count < 100

    await MainActor.run { message2.text = "In Button action - finished" }
    } catch {
    await MainActor.run { message2.text = "In Button action - canceled" }
    }
    }

    Task.detached { [self] in
    await MainActor.run { message1.text = "In Button action - start" }
    try await self.testAsync()
    print("MyButton Pressed step 4")
    tickingTask.cancel()
    }
    }
  3. Or, if you just wanted to do something on the main thread when the async method is done, just put it after the method that you are awaiting:

    @IBAction func didTapButton(_ sender: UIButton) {
    print("MyButton Pressed step 1")

    Task.detached { [self] in
    await MainActor.run { message1.text = "In Button action - start" }
    try await self.testAsync()
    print("MyButton Pressed step 4")

    // put whatever you want on the main actor here, e.g.

    await MainActor.run { message2.text = "In Button action - finished" }
    }
    }
  4. Or, if you wanted a ticking timer on the main thread and you want to cancel it when the async task is done:

    @IBAction func didTapButton(_ sender: UIButton) {
    print("MyButton Pressed step 1")

    var count = 0

    let timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { _ in
    count += 1
    print(count)
    }

    Task.detached { [self] in
    await MainActor.run { message1.text = "In Button action - start" }
    try await self.testAsync()
    print("MyButton Pressed step 4")

    // put whatever you want on the main actor here, e.g.

    await MainActor.run {
    timer.invalidate()
    message2.text = "In Button action - finished"
    }
    }
    }

There are lots of ways to skin the cat. But, the key is that none of these block the main thread (or any thread, for that matter), but we can initiate whatever needs to happen on the main thread at the end of the async task.

iOS Swift: order of code execution / asynchronous execution

Yes, you can be certain that your code will be run in order 1, 2, 3, 4.

Every step in the code you posted is performed synchronously. It completes the entire for loop, then the sort, then assigns to the filtered property, then tells the table view to reload.

Functions that do asynchronous tasks are documented as such. Usually such functions take a completion handler so that you can invoke code once they finish their task.

Async tasks execution with dependency

After Joshua's comment , I able to conclude the answer.


Execution changed from OperationQueue to DispatchGroup and DispatchSemaphore.

DispatchGroup : It makes sure both task tasks are done and then it calls notify block.

DispatchSemaphore : It holds the async resource with wait command until we wont send the signal command i.e. we are saying to semaphore to hold yourself until the task1 is not completed.

Sample code of tasks.

class Execution {
// Managing tasks with DispatchGroup

func executeTaskWithGroup() {
let groups = DispatchGroup()
let semaphore = DispatchSemaphore(value: 1)
groups.enter()
semaphore.wait()
TaskManager.shared.task1Call {
groups.leave()
semaphore.signal()
}

groups.enter()
TaskManager.shared.task2Call {
groups.leave()
}

groups.notify(queue: DispatchQueue.global(qos: .background)) {
print("Tasks Completed")
}

}

}

To execute command all we need to do is.

let e = Execution()
e.executeTaskWithGroup()

But above code is executed in the main thread and block the UI. To prevent this you need to call above piece of code in background queue like below.

let queue = DispatchQueue.init(label: "MyQueue", qos: .background, attributes: .concurrent, autoreleaseFrequency: .workItem, target: nil)

queue.async {
let e = Execution()
e.executeTaskWithGroup()
}

Now everything works fine as per my needed.


AddOn

In case, if someone requirement is to call multiple API along with the above scenario then add your tasks in async in the queue.

let queue = DispatchQueue.init(label: "MyQueue", qos: .background, attributes: .concurrent, autoreleaseFrequency: .workItem, target: nil)

queue.async {
let e1 = Execution()
e1.executeTaskWithGroup()
}

queue.async {
let e2 = Execution()
e2.executeTaskWithGroup()
}

Now both e1 and e2 executes parallelly without blocking main thread.


References :

  • Using Dispatch Group & Semaphore to Group iOS Async Tasks
  • A Quick Look at Semaphores in Swift

Correct way to perform async operations sequentially

You should use group.notify() rather than group.wait(), since the latter is a synchronous, blocking operation.

I also don't see a point of dispatching to a queue if you only dispatch a single work item once.

func fetchResults(for: array, completion: () -> Void) {

var results: [OtherObject]: []
let group = DispatchGroup()
for object in array {
group.enter()
WebService().fetch(for: object) { result in
// Calls back on main queue
// Handle result
results.append(something)

group.leave()
}
}

group.notify(queue: DispatchQueue.main) {
print(results)
completion()
}
}

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")
}
}


Related Topics



Leave a reply



Submit