My original answer (in which I suggest awaiting the prior task) is below. It is a simple pattern that works well, but is unstructured concurrency (complicating cancelation workflows).
Nowadays, I would use an AsyncSequence
, e.g., an AsyncStream
. See WWDC 2021 video Meet AsyncSequence. Or, for queue-like behavior (where we set up the queue initially and later append items to it), more often than not, I reach for AsyncChannel
from the Swift Async Algorithms package. See WWDC 2022 video Meet Swift Async Algorithms.
E.g., I can create an AsyncChannel
for URLs that I want to download:
let urls = AsyncChannel<URL>()
Now that I have a channel, I can set up a task to process them serially with a for
-await
-in
loop:
func processUrls() async {
for await url in urls {
await download(url)
}
}
And, when I later want to add something to that channel to be processed, I can send
to that channel:
func append(_ url: URL) async {
await urls.send(url)
}
You can have every Task
await the prior one. And you can use actor make sure that you are only running one at a time. The trick is, because of actor reentrancy, you have to put that "await prior Task
" logic in a synchronous method.
E.g., you can do:
actor Experiment {
private var previousTask: Task<Void, Error>?
func startSomethingAsynchronous() {
previousTask = Task { [previousTask] in
let _ = await previousTask?.result
try await self.doSomethingAsynchronous()
}
}
private func doSomethingAsynchronous() async throws {
let id = OSSignpostID(log: log)
os_signpost(.begin, log: log, name: "Task", signpostID: id, "Start")
try await Task.sleep(nanoseconds: 2 * NSEC_PER_SEC)
os_signpost(.end, log: log, name: "Task", signpostID: id, "End")
}
}
Now I am using os_signpost
so I can watch this serial behavior from Xcode Instruments. Anyway, you could start three tasks like so:
import os.log
private let log = OSLog(subsystem: "Experiment", category: .pointsOfInterest)
class ViewController: NSViewController {
let experiment = Experiment()
func startExperiment() {
for _ in 0 ..< 3 {
Task { await experiment.startSomethingAsynchronous() }
}
os_signpost(.event, log: log, name: "Done starting tasks")
}
...
}
And Instruments can visually demonstrate the sequential behavior (where the ⓢ
shows us where the submitting of all the tasks finished), but you can see the sequential execution of the tasks on the timeline:

I actually like to abstract this serial behavior into its own type:
actor SerialTasks<Success> {
private var previousTask: Task<Success, Error>?
func add(block: @Sendable @escaping () async throws -> Success) {
previousTask = Task { [previousTask] in
let _ = await previousTask?.result
return try await block()
}
}
}
And then the asynchronous function for which you need this serial behavior would use the above, e.g.:
class Experiment {
let serialTasks = SerialTasks<Void>()
func startSomethingAsynchronous() async {
await serialTasks.add {
try await self.doSomethingAsynchronous()
}
}
private func doSomethingAsynchronous() async throws {
let id = OSSignpostID(log: log)
os_signpost(.begin, log: log, name: "Task", signpostID: id, "Start")
try await Task.sleep(nanoseconds: 2 * NSEC_PER_SEC)
os_signpost(.end, log: log, name: "Task", signpostID: id, "End")
}
}