Swift execute asynchronous tasks in order

Viewed 7628

I have a few asynchronous, network tasks that I need to perform on my app. Let's say I have 3 resources that I need to fetch from a server, call them A, B, and C. Let's say I have to finish fetching resource A first before fetching either B or C. Sometimes, I'd want to fetch B first, other times C first.

Right now, I just have a long-chained closure like so:

func fetchA() {
  AFNetworking.get(completionHandler: {
    self.fetchB()
    self.fetchC()
  })
}

This works for now, but the obvious limitation is I've hard-coded the order of execution into the completion handler of fetchA. Now, say I want to only fetchC after fetchB has finished in that completion handler, I'd have to go change my implementation for fetchB...

Essentially, I'd like to know if there's some magic way to do something like:

let orderedAsync = [fetchA, fetchB, fetchC]
orderedAsync.executeInOrder()

where fetchA, fetchB, and fetchC are all async functions, but fetchB won't execute until fetchA has finished and so on. Thanks!

3 Answers

You could use combination of dispatchGroup and dispatchSemaphore to perform the asynchronous code blocks in sequence.

  • DispatchGroup will maintain the enter and leave to notify when all the task are completed.
  • DispatchSemaphore with value 1 will make sure only one block of task is executed

Sample code where fetchA, fetchB, fetchC are functions with closure (completion handler)

// Create DispatchQueue
private let dispatchQueue = DispatchQueue(label: "taskQueue", qos: .background)

//value 1 indicate only one task will be performed at once.
private let semaphore = DispatchSemaphore(value: 1)

func sync() -> Void {
    let group = DispatchGroup()

    group.enter()
    self.dispatchQueue.async {
        self.semaphore.wait()
        fetchA() { (modelResult) in
            // success or failure handler

            // semaphore signal to remove wait and execute next task
            self.semaphore.signal()
            group.leave()
        }
    }

    group.enter()
    self.dispatchQueue.async {
        self.semaphore.wait()
        fetchB() { (modelResult) in
            // success or failure handler

            // semaphore signal to remove wait and execute next task
            self.semaphore.signal()
            group.leave()
        }
    }

    group.enter()
    self.dispatchQueue.async {
        self.semaphore.wait()
        fetchC() { (modelResult) in
            // success or failure handler

            // semaphore signal to remove wait and execute next task
            self.semaphore.signal()
            group.leave()
        }
    }        

    group.notify(queue: .main) {

        // Perform any task once all the intermediate tasks (fetchA(), fetchB(), fetchC()) are completed. 
        // This block of code will be called once all the enter and leave statement counts are matched.

    }
}

Not sure why other answers are adding unnecessary code, what you are describing is already the default behavior for a serial queue:

let fetchA = { print("a starting"); sleep(1); print("a done")}
let fetchB = { print("b starting"); sleep(1); print("b done")}
let fetchC = { print("c starting"); sleep(1); print("c done")}

let orderedAsync = [fetchA, fetchB, fetchC]
let queue = DispatchQueue(label: "fetchQueue")

for task in orderedAsync{
    queue.async(execute: task) //notice "async" here
}

print("all enqueued")
sleep(5)

"all enqueued" will print immediately, and each task will wait for the previous one to finish before it starts.

FYI, if you added attributes: .concurrent to your DispatchQueue initialization, then they wouldn't be guaranteed to execute in order. But even then you can use the .barrier flag when you want things to execute in order.

In other words, this would also fulfill your requirements:

let queue = DispatchQueue(label: "fetchQueue", attributes: .concurrent)

for task in orderedAsync{
    queue.async(flags: .barrier, execute: task)
}
Related