George
George

Reputation: 30491

Concurrently run async tasks with unnamed async let

With Swift concurrency, is it possible to have something almost like an 'unnamed' async let?

Here is an example. You have the following actor:

actor MyActor {
    private var foo: Int = 0
    private var bar: Int = 0

    func setFoo(to value: Int) async {
        foo = value
    }

    func setBar(to value: Int) async {
        bar = value
    }

    func printResult() {
        print("foo =", foo)
        print("bar =", bar)
    }
}

Now I want to set foo and bar using the given methods. Simple usage would look like the following:

let myActor = MyActor()
await myActor.setFoo(to: 5)
await myActor.setBar(to: 10)
await myActor.printResult()

However this code is sequentially run. For all intents and purposes, assume setFoo(to:) and setBar(to:) may be a long running task. You can also assume the methods are mutually exclusive (don't share variables & won't affect each other).

To make this code current, async let can be used. However, this just starts the tasks until they are awaited later on. In my example you'll notice I don't need the return value from these methods. All I need is that before printResult() is called, the previous tasks have completed.

I could come up with the following:

let myActor = MyActor()
async let tempFoo: Void = myActor.setFoo(to: 5)
async let tempBar: Void = myActor.setBar(to: 10)
let _ = await [tempFoo, tempBar]
await myActor.printResult()

Explicitly creating these tasks and then awaiting an array of them seems incorrect. Is this really the best way?

Upvotes: 2

Views: 1698

Answers (2)

George
George

Reputation: 30491

This can be achieved with a task group using withTaskGroup(of:returning:body:). The method calls are individual tasks, and then we await waitForAll() which continues when all tasks have completed.

Code:

await withTaskGroup(of: Void.self) { group in
    let myActor = MyActor()

    group.addTask {
        await myActor.setFoo(to: 5)
    }
    group.addTask {
        await myActor.setBar(to: 10)
    }

    await group.waitForAll()
    await myActor.printResult()
}

Another method would be like so:

let myActor = MyActor()
await withTaskGroup(of: Void.self) { group in
    group.addTask {
        await myActor.setFoo(to: 5)
    }
    group.addTask {
        await myActor.setBar(to: 10)
    }
}
await myActor.printResult()

Upvotes: 4

Jano
Jano

Reputation: 63707

I made your actor a class to allow concurrent execution of the two methods.

import Foundation

final class Jeep {
    private var foo: Int = 0
    private var bar: Int = 0

    func setFoo(to value: Int) {
        print("begin foo")
        foo = value
        sleep(1)
        print("end foo \(value)")
    }

    func setBar(to value: Int) {
        print("begin bar")
        bar = value
        sleep(2)
        print("end bar \(bar)")
    }

    func printResult() {
        print("printResult foo:\(foo), bar:\(bar)")
    }
}

let jeep = Jeep()
let blocks = [ 
    { jeep.setFoo(to: 1) }, 
    { jeep.setBar(to: 2) },
]

// ...WORK

RunLoop.current.run(mode: RunLoop.Mode.default, before: NSDate(timeIntervalSinceNow: 5) as Date)

Replace WORK with one of these:

// no concurrency, ordered execution
for block in blocks {
    block() 
}
jeep.printResult()


// concurrency, unordered execution, tasks created upfront programmatically
Task {
    async let foo: Void = blocks[0]()
    async let bar: Void = blocks[1]()
    await [foo, bar]
    jeep.printResult()
}

// concurrency, unordered execution, tasks created upfront, but started by the system (I think)
Task {
    await withTaskGroup(of: Void.self) { group in
        for block in blocks {
            group.addTask { block() }
        } 
    }
    // when the initialization closure exits, all child tasks are awaited implicitly
    jeep.printResult()
}

// concurrency, unordered execution, awaited in order
Task {
    let tasks = blocks.map { block in
        Task { block() } 
    }
    for task in tasks {
        await task.value
    }
    jeep.printResult()
}

// tasks created upfront, all tasks start concurrently, produce result as soon as they finish
let stream = AsyncStream<Void> { continuation in
    Task {
        let tasks = blocks.map { block in
            Task { block() }
        }
        for task in tasks {
            continuation.yield(await task.value)
        }
        continuation.finish()
    }
}
Task {
    // now waiting for all values, bad use of a stream, I know
    for await value in stream { 
        print(value as Any)
    }
    jeep.printResult()
}

Upvotes: 0

Related Questions