Reputation: 1
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
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:
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