Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How does await in swift work with tuples?

I'm trying to make sure I understand the behavior of await. Suppose we have the following functions:

func do() async {
  //code
}
func stuff() async {
  //code
}

The following statements will run do and stuff sequentially:

await do()
await stuff()

But the following statement will run do and stuff in parallel correct?

await (do(), stuff())

I'm not sure how to check in Xcode if my code runs in parallel or in sequence.

like image 805
gloo Avatar asked Oct 15 '25 19:10

gloo


2 Answers

Short answer:

If you want concurrent execution, either use async let pattern or a task group.


Long answer:

You said:

But the following statement will run do and stuff in parallel correct?

await (do(), stuff()) 

No, they will not.

This is best illustrated empirically by:

  • Make the task take enough time that concurrency behavior can easily be manifested; and
  • Use “Points of Interest” instrument (e.g., by picking the “Time Profiler” template) in Instruments to graphically represent intervals graphically over time.

Consider this code, using the tuple approach:

import os.log

actor Experiment {
    private let logger = OSSignposter(subsystem: Bundle.main.bundleIdentifier!, category: .pointsOfInterest)

    func example() async {
        let values = await (doSomething(), doSomethingElse())
        print(values)
    }

    func doSomething() async -> Int {
        spin(#function)
        return 1
    }

    func doSomethingElse() async -> Int {
        spin(#function)
        return 42
    }

    func spin(_ name: StaticString) {
        let state = logger.beginInterval(name, id: logger.makeSignpostID(), "begin")

        let start = ContinuousClock.now
        while start.duration(to: .now) < .seconds(1) { }   // spin for one second

        logger.endInterval(name, state, "end")
    }
}

That results in a graph that shows that it is not happening concurrently:

enter image description here

Whereas:

func example() async {
    async let foo = doSomething()
    async let bar = doSomethingElse()
    let values = await (foo, bar)
    print(values)
}

That does result in concurrent execution:

enter image description here


Now, in the above examples, I changed the functions so that they returned values (as that is really the only context where using tuples makes any practical sense).

But if they did not return values and you wanted them to run in parallel, you might use a task group:

func experiment() async {
    await withDiscardingTaskGroup { group in
        group.addTask { await self.doSomething() }
        group.addTask { await self.doSomethingElse() }
    }
}

func doSomething() async {
    spin(#function)
}

func doSomethingElse() async {
    spin(#function)
}

That also results in the same graph where they run in parallel.

You can also just create Task instances and then await them:

func experiment() async {
    let task1 = Task { await doSomething() }
    let task2 = Task { await doSomethingElse() }

    await withTaskCancellationHandler {
        _ = await task1.value
        _ = await task2.value
    } onCancel: {
        task1.cancel()
        task2.cancel()
    }
}

As you can see, if you use unstructured concurrency, you have to handle cancelation manually.

However, task groups offer greater flexibility when the number of created tasks may not be known at compile-time.

like image 150
Rob Avatar answered Oct 17 '25 09:10

Rob


No, they are not executed in parallel.

Firstly,

await (do(), stuff())

is shorthand syntax for

(await do(), await stuff())

which is evaluated from left to right, just like any other lines of code.

Now, any await call suspends the execution of the caller, if the callee suspends. This means the execution will continue with await stuff() only after await do() finishes.

await is a suspension point (actually a possible suspension point), this means the execution won't continue until that await complete (in either an async or a sync manner, it doesn't matter).


I'm not sure how to check in Xcode if my code runs in parallel or in sequence

You don't need to, unless you don't trust the compiler. The only ways to spawn multiple await from the same async context (task) is via async-let and TaskGroup.

But if you really want to check, for learning purposes, others have provided some interesting approaches. I'm just gonna leave mine, which is similar to another already provided one:

func delay(for interval: TimeInterval = 2.0, marker: StaticString = #function) async {
    print("\(marker) begin")
    try? await Task.sleep(nanoseconds: UInt64(interval * 1_000_000))
    print("\(marker) end")
}

func `do`() async {
    await delay()
}

func stuff() async {
    await delay()
}

Task {
    let result = await (`do`(), stuff())
}

The above code results in the following output:

do() begin
do() end
stuff() begin
stuff() end
like image 30
Cristik Avatar answered Oct 17 '25 11:10

Cristik



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!