EmilyYu
EmilyYu

Reputation: 1

Run function after two functions has ran

So let's say I have these three functions:

func func_1() {
    Task { @MainActor in
        let state = try await api.get1State(v!)
        print("cState func_1: \(state!)")
    }
}

func func_2() {
    Task { @MainActor in
        let state = try await api.get2State(v!)
        print("cState func_2: \(state!)")
    }
}

func func_3() {
    Task { @MainActor in
        let state = try await api.get3State(v!)
        print("cState func_3: \(state!)")
    }
}

Since these function get info from api, it might take a few seconds.

How can I run func_3, after both func_1 and func_2 is done running?

Upvotes: 0

Views: 174

Answers (1)

Rob
Rob

Reputation: 437622

I would advise avoiding Task (which opts out of structured concurrency, and loses all the benefits that entails) unless you absolutely have to. E.g., I generally try to limit Task to those cases where I are going from a non-asynchronous context to an asynchronous one. Where possible, I try to stay within structured concurrency.

As The Swift Programming Language: Concurrency says:

Tasks are arranged in a hierarchy. Each task in a task group has the same parent task, and each task can have child tasks. Because of the explicit relationship between tasks and task groups, this approach is called structured concurrency. Although you take on some of the responsibility for correctness, the explicit parent-child relationships between tasks lets Swift handle some behaviors like propagating cancellation for you, and lets Swift detect some errors at compile time.

And I would avoid creating functions (func_1, func_2, and func_3) that fetch a value and throw it away. You would presumably return the values.

If func_1 and func_2 return different types, you could use async let. E.g., if you're not running func_3 until the first two are done, perhaps it uses those values as inputs:

func runAll() async throws {
    async let foo = try await func_1()
    async let bar = try await func_2()

    let baz = try await func_3(foo: foo, bar: bar)
}

func func_1() async throws -> Foo {
    let foo = try await api.get1State(v!)
    print("cState func_1: \(foo)")
    return foo
}

func func_2() async throws -> Bar {
    let bar = try await api.get2State(v!)
    print("cState func_2: \(bar)")
    return bar
}

func func_3(foo: Foo, bar: Bar) async throws -> Baz {
    let baz = try await api.get3State(foo, bar)
    print("cState func_3: \(baz)")
    return baz
}

Representing that visually using “Points of Interest” tool in Instruments:

enter image description here

The other pattern, if func_1 and func_2 return the same type, is to use a task group:

func runAll() async throws {
    let results = try await withThrowingTaskGroup(of: Foo.self) { group in
        group.addTask { try await func_1() }
        group.addTask { try await func_2() }

        return try await group.reduce(into: [Foo]()) { $0.append($1) } // note, this will be in the order that they complete; we often use a dictionary instead
    }
    let baz = try await func_3(results)
}

func func_1() async throws -> Foo { ... }

func func_2() async throws -> Foo { ... }

func func_3(_ values: [Foo]) async throws -> Baz { ... }

There are lots of permutations of the pattern, so do not get lost in the details here. The basic idea is that (a) you want to stay within structured concurrency; and (b) use async let or TaskGroup for those tasks you want to run in parallel.


I hate to mention it, but for the sake of completeness, you can used Task and unstructured concurrency. From the same document I referenced above:

Unstructured Concurrency

In addition to the structured approaches to concurrency described in the previous sections, Swift also supports unstructured concurrency. Unlike tasks that are part of a task group, an unstructured task doesn’t have a parent task. You have complete flexibility to manage unstructured tasks in whatever way your program needs, but you’re also completely responsible for their correctness.

I would avoid this because you need to handle/capture the errors manually and is somewhat brittle, but you can return the Task objects, and await their respective result:

func func_1() -> Task<(), Error> {
    Task { @MainActor [v] in
        let state = try await api.get1State(v!)
        print("cState func_1: \(state)")
    }
}

func func_2() -> Task<(), Error> {
    Task { @MainActor [v] in
        let state = try await api.get2State(v!)
        print("cState func_2: \(state)")
    }
}

func func_3() -> Task<(), Error> {
    Task { @MainActor [v] in
        let state = try await api.get3State(v!)
        print("cState func_3: \(state)")
    }
}

func runAll() async throws {
    let task1 = func_1()
    let task2 = func_2()

    let _ = await task1.result
    let _ = await task2.result
    let _ = await func_3().result
}

Note, I did not just await func_1().result directly, because you want the first two tasks to run concurrently. So launch those two tasks, save the Task objects, and then await their respective result before launching the third task.

But, again, your future self will probably thank you if you remain within the realm of structured concurrency.

Upvotes: 1

Related Questions