Serializing Asynchronous Operations Using Closures and Private Functions
Sometimes you need to perform an asynchronous task, like downloading a file. In swift, closures come in very handy to perform some operation once the asynchronous task completes. For example:
func downloadFile(fromURL url:NSURL, completion:((error:NSError?)->()))
{
MyHTTPLibrary.sendHTTPRequest({ response in
if response.statusCode == 200 {
// SUCCESS
completion(error:nil)
}
else{
// FAILURE:
completion(error: response.error!)
}
})
}
And some other times, you need to perform a lot of asynchronous tasks (like
-
for example- downloading a lot of files). We could expand the previous code with a
for
loop, like this:func downloadFiles(fromURLs urls:[NSURL], completion:((error:NSError?)->())) { for url in urls { self.downloadFile(fromURL:url, completion:{ error in if error != nil { completion(error:error) break }
if url == urls.lastObject(){ completion(error:nil) } }) } }
This will loop through all the URLs and send an asynchronous request for each. The problem is, all the requests are
sent almost at the same time: Most likely, you will finish sending the last request before you get the first response.
Moreover, we have setup our success trigger (completion(error:nil)
) at the end of the completion handler of the last request. This assumes that the last request will complete last: a race condition.
A better design requires that we serialize (or enqueue) all our requests, so that they are performed one at a time. This way, no matter how long each request takes, we can be sure that request number N+1 is sent only after we received a response to request number N. Also, we don’t need to worry about our HTTP library having to deal with too many concurrent connections.
In a delegate-based scenario, we would have a mutable array of connection tasks serving as a queue, and dequeue them one by one on the delegate method that is called on completion, to start the new task.
But with colsures, we have all our code on-site (no separate methods), so at first it is not clear how to “serialize” all our downloads. However, with a clever trick we can accomplish this:
func downloadFiles(fromURLs urls:[NSURL], completion:((error:NSError?)->()))
{
// [1] Define recursion:
var mutableURLs = urls
func downloadNext()
{
if mutableURLs.count == 0 {
// Terminating condition
return completion(error:nil)
}
let url = urls.removeFirst()
self.downloadFile(fromURL:url, completion:{error in
guard error == nil else {
// Failure; abort:
completion(error:error)
return
}
// Success; recurse:
donwloadNext()
})
}
// [2] Kickstart recursion :
donwloadNext()
}
The code runs as follows:
- We make a mutable copy of the URL array.
- Call
downloadNext()
for the first time: the array is not empty. We remove first URL and send an asynchronous request to it. - Return from
downloadNext()
right away. - Return from
downloadFiles()
right away. - …
- On request completion,
downloadNext()
calls itself -this time, with the diminished URL array- and the cycle goes on, until the array is empty anddownloadNext()
bails out to success.
All in all, downloadNext()
is called N+1 times to perform N requests (the last time finds the URL array emoty and bails out).