You signed in with another tab or window.Reload to refresh your session.You signed out in another tab or window.Reload to refresh your session.You switched accounts on another tab or window.Reload to refresh your session.Dismiss alert
Async is a free-to-use framework of functions and types that I have found incredibly useful in working withGrand Central Dispatch, although the work horses of the library,Future and its corespondingPromise, are useful in a wider variety of contexts. I've been using these in my own code for a long time, and have always meant to share them, so now I finally am. I have other types and functions I have found useful that I may add later, but these form the core functionality I use nearly every time I use GCD.
Even thoughFuture is really the central feature, the library is calledAsync because it provides globalasync free functions and adds coresponding methods onDispatchQueue that return aFuture, so I almost never need to explictly create aPromise , and often theFuture itself just disappears behind fluid completion handler syntax, so that it almost seems as though the library is aboutasync. But actuallyFuture is the hero, and it's more flexible than mostFuture implementations I've seen.
I often try an idea by using writing a command line tool rather than an actual AppKit/UIKit application, so this package is specifically designed to be independent of those. It uses only GCD and Foundation.
What's Included
async
There are a few variations of a globalasync free function that immediately return aFuture, asynchronously executing your closure, which can safely throw an error, on a global default concurrentDispatchQueue. You don't need to create a queue explictly yourself, unless you just need it for some other reason.
I provide a few variations, because I find it annoying that it's necessary to specify a deadline in GCD'sasyncAfter as aDispatchTime. I almost always want my code to run either as soon as possible or after a specified delay from the moment I calledasync, and nearly never need it to run at a specific point in time. So I provide variations on myasync function that allow you specify a deadline as aDispatchTime, as with GCD's native version, or a delay asDispatchTimeInterval orTimeInterval.
sync
There aresynchronous versons of the globalasync free functions. They merely run your closure immediately in the current thread as though you had called it directly yourself. This is useful for several reasons:
• It allows an intermediate step in refactoring synchronous code into asynchronous during which you are still executing a closure synchronously while getting aFuture from it. In that case, theFuture is ready immediately when your closure returns, and if you added handlers, they are called immediately. Then by simply changingsync toasync, it becomes truly asynchronous.
• It is sometimes helpful for debugging to usesync instead ofasync temporarily, and it is a simple one-letter code change.
• It returns aFuture, so you can use it inside of your own code as an easy way to return aFuture without worrying about how to usePromise, althoughPromise is pretty easy to use directly.
DispatchQueue extension
async andsync methods have been added toDispatchQueue to corespond to their global versions, but you get theFuture returning behavior on the queue of your choice instead of the default global concurrent queue.
Mutex
Mutex is a class that simplifies guarding shared data for thread safety by providing awithLock method to automate locking and unlocking the mutex, which is what should be used most of the time; however, recognizing that sometimes it's necesary to interleave locking and unlocking different mutexes in ways that aren't always conveniently nestable, it also provides explicitlock,unlock methods as well as a failabletryLock method that allows you to specify a time-out.
Future
This implementation allows you use the typical fluid style you see in most libraries, but it also allows you to use theFuture as a genuine place-holder for a value, similar to C++'sstd::future, that you can store away and query when you need it. It also allows you to specify a timeout for both usages.
Promise
Promise is the necessary but usually hidden counterpart ofFuture. Even though its interface is really simple, if you mainly just need to get aFuture fromasync, you'll never need to create aPromise yourself. On the other hand, you can usePromise to return aFuture from any code you like, for example your own wrapper around a third party asynchronous library.
Basic Usage
Although each function and type is fully documented with markup comments, so in Xcode you can easily get a reference with QuickLook, it is helpful to see at least basic examples as a starting point. I'll present these in the order I think most people will use them.
Future and async
Future doesn't make a lot of sense in isolation, so I'll describe it in conjuntion withasync, but understand that fundamentally the only way to obtain aFuture directly is from aPromise, which will be described later.async usesPromise internally to return aFuture. There's nothing special about this, and you can do that yourself to useFuture in other contexts.
You can think of aFuture in several ways, and they are all simultaneously true:
It is the receiver side of a one-shot, one-way communicaton channel for the result of a closure.Promise is the sender end of that channel.
It is a placeholder for some result to be set by some other code in the future, possibly in another thread of execution.
It is a means of specifying completion handlers to be executed when a closure returns or throws, possibly in another thread of execution.
Attaching handlers (callbacks) to aFuture is the easiest and safest way to keep your code from blocking on a result, since you can just continue on doing something else after attaching them, and your handlers will be called automatically when your asynchronous code completes. In terms of ease of use and safety, handlers are normally the way to go.
They do have some drawbacks though. If you're combining the results of several asynchronous tasks that must all complete before progress can be made, handlers can be awkward. For example, in a computation graph, all input computations must complete before the current node can be evaluated. In that case the current computation should block until the inputs are ready. Of course, you could force the square peg of handlers into this round hole, but that's not a good use case for handlers. You'd need to update some counter for inputs, ensure that updates to that counter from multiple handlers happen atomicly, and block on that counter. It's not that hard, but what a needless, inefficient and error-prone mess! By comparison, it's ideal for using collection ofFutures as placeholders. Just iteratively wait on each of them. When the loop completes they're all ready, and you can proceed to evaluate the current node. This is exactly why I provide that alternative.
On the other hand, fetching data from a network or responding to events, very common cases, are usually ideal uses of handlers, and keeping aFuture for a placeholder becomes the awkward one, because it requires you either to block until it's ready, or to write some kind of loop or other scheme to continually check when it's ready while your code continues.
Having said that completion handlers are a good fit for callbacks, such as for HTTP requests, using them as placeholders is a good way to avoid deeply nested callbacks when the results of one request require spawning another request, and another.
The implementation ofasync in this package, differs from GCD's nativeasync andasyncAfter methods in two ways. The first is that it returns aFuture, and the second is that there are global free function variants that use a default concurrentDispatchQueue, in addition to methods onDispatchQueue itself.
When you callasync, it schedules your closure for execution, using GCD's nativeasync, but it also immediately returns aFuture for the value your closure will return, or the error it will throw. You can hold on to thisFuture as a means for querying for your closure's eventual result, or you can use it to attach handlers... or both. The two ways of using it can be used together, if that makes sense for your application.
Future handler attachment
.onSuccess and.onFailure
As a basic example, let's say we have a long-running function,foo() throws -> Int. We can schedulefoo withasync, and attach handlers to the returnedFuture like so:
foo will run concurrently, and if it eventually returns a value, the closure passed to.onSuccess will be called with that value. If on the other hand,foo throws, the closure passed to.onFailure is called with the error.
Notice how theFuture doesn't explicitly appear in the above code, but it's there. It's whatasync returns, in this case,Future<Int>, and it's thatFuture's.onSuccess method that we're calling to specify the success handler..onSuccess returns the sameFuture, which allows us to chain a.onFailure method call to schedule our failure handler. It is equivalent to:
You can specify as many handlers as you like, mixing and matching, completion handlers, success handlers, and failure handlers. All applicable handlers will be called concurrently. This allows you to return the future you got fromasync so that code further up the call chain can attach their own handlers.
.timeout
You can also specify a time-out for theFuture using the same fluid style. If the specified time-out elapses before the closure completes, an error is set in theFuture and its.onFailure and.onCompletion handlers are called. See the comment documentation forFuture for more information on that.
Future as a placeholder
As an alternative to the fluid, functional-like, usage above, you can useFuture in a more traditionally imperative way, as a placeholder for a yet to be determined value. Used this way, it's much more like C++'sstd::future. This is especially useful when you useasync to subdivide a larger task into to a number of concurrent subtasks, which must be combined into a final result before continuing.
When usingFuture as a placeholder, you store it away as you might store the actual value returned by the asynchronous code or the error thrown by it, if it had been called synchronously, and then query theFuture for the value or error some time later when you need it. To support this,Future provides blocking properties and methods to query the future and wait for it to be ready, as well as a non-blocking property to query its ready state.
Any handlers that have been attached will still be run, whether or not you useFuture as a placeholder. The two styles of use can be used together.
Blocking methods and properties
Be aware that in an AppKit/UIKit application, using blocking methods and properties in the main thread can make your app unresponsive while they block. Either use them in separate thread that can safely block, use.isReady to do something else when theFuture is not ready, ensure that all asynchronous calls will complete quickly, or just avoid blocking methods and properties altogether by attaching handlers instead.
.value and.error:
You can obtain the value or error from the future with its.value and.error properties. We'll use the samefoo from the previous examples:
letfuture=async{returntryfoo()}// future.value and future.error will block until the foo returns or throwsiflet value= future.value{print("foo returned\(value)")}elseiflet error= future.error{print("foo threw exception,\(error.localizedDescription)")}
These propertiesonly return when the future is ready, meaning thatfoo has either returned a value or thrown an error. Until then, they just block, waiting forfoo to complete. Whenfoo does complete, one of the following will be true:
If it returns a value,.value will contain that value, and.error will benil.
If it throws an error,.value will benil, and.error will contain the error.
TheFuture will never have both an error and a value.
Note that if atimeout modifer was set as mentioned above, and the specified time-out elapses before the closure completes,.error will containFutureError.timedOut.
.result
If you prefer to use Swift'sResult type, you can access the.result property instead of.value and.error:
letfuture=async{returntryfoo()}// future.result will block until future is readyswitch future.result{caselet.success(value):print("foo returned\(value)")caselet.failure(error):print("foo threw exception,\(error.localizedDescription)")}
.result will block in exactly the same way as.value and.error.
If atimeout modifer was set as mentioned above, and the specified time-out elapses before the closure completes,.result will be.failure(FutureError.timedOut).
.getValue()
If you prefer to usedo {...} catch {...} blocks for error handling,Future provides a throwing.getValue() method:
letfuture=async{returntryfoo()}// future.getValue() will block until the foo returns or throwsdo{letvalue=try future.getValue()print("foo returned\(value)")}catch{print("foo threw exception,\(error.localizedDescription)")}
.wait()
If you don't need the actual result (perhaps your closure returnsVoid), you can simply call the.wait() method
letfuture=async{let _=tryfoo()}future.wait() // block until future is ready// Now the future is ready, so do other stuff
.wait() also has a time-out variant. It is different from thetimeout modifier mentioned above in that it does not set an error in theFuture when it times out. It merely stops waiting for theFuture to be ready, throwing an error itself when it times out. Refer toFuture's comment documentation for more information.
Non-blocking methods and properties
.isReady
The blocking behavior of.wait(),.value ,.error and.result is useful if the code following them depends on the value returned or error thrown byfoo, but it can be a problem if you could do other work while you wait for theFuture to be ready, because it stops your current thread in its tracks untilfoo is done, in which case, you don't get much value from usingasync. For this reason, the ability to determine if theFuture is ready without blocking is essential. You can do this with the.isReady property:
letfuture=async{returntryfoo()}while !future.isReady{ // Do some other work while we wait}iflet value= future.value{print("foo returned\(value)")}elseiflet error= future.error{print("foo threw exception,\(error.localizedDescription)")}
If you're not going to do other work while you wait for theFuture to be ready, it's far more efficient to call.wait() rather than looping on.isReady, because all ofFuture's blocking methods and properties, including.wait(), useDispatchSemaphore under the hood, which can truly suspend the thread, whereas spinning on.isReady will consume CPU cycles unnecessarily.
async variations
All of theasync variants allow you to specify quality of service (qos), and flags just as the GCD nativeasync andasyncAfter do, and use the same default values when you don't specify them.
The following examples use the global async free functions, but theDispatchQueue extension provides equivalent instance methods with the same signatures as the global functions, so you can call them on a specificDispatchQueue.
.async()
If you want your closure to be executed as soon as possible, you can call it as the previous examples with no deadline or delay interval.
letfuture=async{returntryfoo()}
.async(afterDeadline:)
If you need to delay execution of your closure until a specific point in time, you can use theafterDeadline variant to specify aDispatchTime orDate
letdeadline:DispatchTime=.now()+.milliseconds(1000)// Some time later...letfuture=async(afterDeadline: deadline){returntryfoo()}
.async(afterInterval:)
To specify a delay interval using justDispatchTimeInterval, you can use theafterInterval variant
Alternatively, you can specify the delay as aTimeInterval in seconds, using theafterSeconds variant
letfuture=async(afterSeconds:1){returntryfoo()}
sync
sync is the synchronous dual ofasync. It runs the closure passed to it in the current thread before returning.
If no deadline or delay is specified, it will execute the closure immediately. If a deadline or delay is specified, it will block until the deadline, or delay has elapsed, and then execute the closure. Becausesync doesn't return until the closure has been executed, any handlers attached to theFuture it returns will be executed as soon as they are attached, if they apply.
Otherwise everything said forasync applies tosync.
Mutex
One unfortunate side effect of concurrent code, such as that executed byasync, is that any mutable shared data that could be accessed by multiple tasks simultaneously must be guarded to avoid data races. That's whatMutex is for. You create aMutex to guard some data, and then lock it before accessing the shared data, and unlock it afterwards. Explicitly having to lock and unlock theMutex is error prone,so the preferred way to use this implementation ofMutex is through itswithLock method. As an example, here's a simple implemenation of aSharedStack:
withLock blocks until the a lock can be obtained, and once obtained, it executes the closure passed to it, and unlocks before returning. The unlock happens immediately after the closure completes, regardless of whether it returns or throws.
Mutex also provides a failablewithAttemptedLock method that allows you specify a time-out, possibly none, after which it will stop waiting for a lock and throwsMutexError.lockFailed. If it fails, the lock is not obtained and the closure is not run.
Although explicitly locking and unlocking theMutex is error-prone, there are circumstances when neitherwithLock norwithAttemptedLock will do the job, such as interleaving locking and unlocking multiple mutexes in ways that are neither exclusive to one another, nor cleanly nestable. For those cases,Mutex also provideslock(),tryLock(), andunlock() methods.PreferwithLock andwithAttemptedLock when you can use them.If you must explicitly lock and unlock the mutex yourself, it is your responsibility to ensure that eachlock() or successfultryLock() is balanced by exactly oneunlock(), otherwise, you'll deadlock, or crash when theMutex is deinitialized. This crashing behavior is one that is inherited by its being implemented in terms ofDispatchSemaphore which crashes when deinitialized with a negative value, which it will have if theMutex is still locked. This is actually a good thing because it tells you unambiguously that you have a bug. To paraphrase Apple's documentation on the subject: Don't do that!
Refer to comment documentation for more information on these other methods.
Promise
Promise is the sender of thePromise/Future team. It's how you obtain aFuture to return from your own code, and how you set the value in theFuture from code that may be executed far removed from the code receiving theFuture, possibly in a completely different thread.
.set(from:)
If you wish to return aFuture in your own custom code, you do so by creating aPromise and returning its.future property in the immediate context, while passing the closure that returns the value, possibly throwing an error, to the.set(from:) method in the dispatched context.
As an example, let's suppose you want to wrapURLSession's.dataTask to return aFuture to the resultingData.
extensionURLSession{func dataFuture(with url:URL)->Future<Data>{letpromise=Promise<Data>()lettask=self.dataTask(with: url){(data, response, error)in // ignoring response for this example promise.set{iflet error= error{throw error}return data??Data()}} task.resume()return promise.future}}
The.set(from:) method will set theFuture according to whether the closure returns or throws, which in this case depends on whetherdataTask calls its completion handler with an error:
If.dataTask calls its completion handler with a non-nil error, the closure passed to.set(from:) will throw, causing thePromise to set theFuture's.error.
If.dataTask calls its completion handler with anil error, the closure passed to.set(from:) will returndata causing thePromise to set theFuture'svalue todata.
In this example, we ignoreresponse, and since we return aFuture instead of aURLSessionDataTask, so we also resume the task returned fromdataTask before returning.
.setResult(from:)
Promise also provides asetResult(from:) method that takes anon-throwing closure that returns a SwiftResult:
extensionURLSession{func dataFuture(with url:URL)->Future<Data>{letpromise=Promise<Data>()lettask=self.dataTask(with: url){(data, response, error)in // ignoring response for this example promise.setResult{()->Result<Data,Error>iniflet error= error{return.failure(error)}return.success(data??Data())}} task.resume()return promise.future}}
About
Swift Async package that makes creating and using asynchronous tasks easier.