Home > Mobile >  Concurrently run async tasks with unnamed async let
Concurrently run async tasks with unnamed async let

Time:02-04

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) {
        foo = value
    }

    func setBar(to value: Int) {
        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?

CodePudding user response:

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

CodePudding user response:

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()
}
  •  Tags:  
  • Related