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")
}
}
OperationQueue(developer.apple.com/documentation/foundation/operationqueue). You will submit all calls asOperations to theOperationQueue, which will havemaxConcurrentOperationCount = 1, and that will ensure that only one operation in the queue is executed at any point. It means of course that operation should be removed from queue not when it starts, but when it receives the response (but all operations can wait in the queue).awaitthe prior task. The challenge is that actors are reentrant, so you need a synchronous tasks that awaits the prior task and starts the next asynchronous task. SeeSerialTasksimplementation in stackoverflow.com/a/70586879/1271826.