SwiftNIO: Understanding Futures and Promises
SwiftNIO is Apple non-blocking networking library. It can be used to write either client libraries or server frameworks and works on macOS, iOS and Linux.
It is built by some of the Netty team members. It is a port of Netty, a high performance networking framework written in Java and adapted in Swift. SwiftNIO thus reuses years of experience designing a proven framework.
We can help you build your next iOS or macOS app in weeks rather than months
Read more about ProcessOne and Swift »
If you want to understand in depth how SwiftNIO works, you first have to understand underlying concept. I will start in this article by explaining the concept of futures and promises. The ‘future’ concept is available in many languages, including Javascript and C#, under the name async / await, or in Java and Scala, under the name ‘future’.
Futures and promises
Futures and promises are a set of programming abstractions to write asynchronous code. The principle is quite simple: Your asynchronous code will return a promise instead of the final result. The code calling your asynchronous function is not blocked and can do other operations before it finally decides to block and wait for the result, if / when it really needs to.
Even if the words ‘futures’ and ‘promises’ are often use interchangeably, there is a slight difference in meaning. They represent different points of view on the same value placeholder. As explained in Wikipedia page:
A future is a read-only placeholder view of a variable, while a promise is a writable, single assignment container which sets the value of the future.
In other words, the future is what the client code receives and can use as a handler to access a future value when it has been defined. The promise is the handler the asynchronous code will keep to write the value when it is ready and thus fulfill the promise by returning the future value.
Let’s see in practice how futures and promises work.
SwiftNIO comes with a built-in futures and promises library. The code lies in EventLoopFuture. Don’t be fooled by the name: It is a full-featured ‘future’ library that you can use in your code to handle asynchronous operations.
Let’s see how you can use it to write asynchronous code, without specific reference to SwiftNIO-oriented networking operations.
Note: The examples in this blog post should work both on macOS and Linux.
Anatomy of SwiftNIO future / promise implementation
Step 1: Create an EventLoopGroup
The basic skeleton for our example is as follow:
import NIO
let evGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
// Do things
try evGroup.syncShutdownGracefully()
We create an EventLoopGroup and shut it down gracefully at the end. A graceful shutdown means it will properly terminate the asynchronous jobs being executed.
An EventLoopGroup can be seen as a provider of an execution context for your asynchronous code. You can ask the EventLoopGroup for an execution context: an EventLoop. Basically, each execution context, each EventLoop is a thread. EventLoops are used to provide an environment to run your your concurrent code.
In the previous example, we create as many threads as we have cores on our computer (System.coreCount), but the number of threads could be as low as 1.
Step 2: Getting an EventLoop to execute your promise
In SwiftNIO, you cannot model concurrent execution without at least an event loop. For more info on what I mean by concurrency, you can watch Rob Pike excellent talk: Concurrency is not parallelism.
To execute your asynchronous code, you need to ask the EventLoopGroup for an EventLoop. You can use the method next()
to get a new EventLoop, in a round-robin fashion.
The following code gets 10 event loops, using the next()
method and prints the event loops information.
import NIO
print("System cores: \(System.coreCount)\n")
let evGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
for _ in 1...10 {
let ev = evGroup.next()
print(ev)
}
// Do things
try evGroup.syncShutdownGracefully()
On my system, with 8 cores, I get the following result:
System cores: 8
SelectableEventLoop { selector = Selector { descriptor = 3 }, scheduledTasks = PriorityQueue(count: 0): [] }
SelectableEventLoop { selector = Selector { descriptor = 4 }, scheduledTasks = PriorityQueue(count: 0): [] }
SelectableEventLoop { selector = Selector { descriptor = 5 }, scheduledTasks = PriorityQueue(count: 0): [] }
SelectableEventLoop { selector = Selector { descriptor = 6 }, scheduledTasks = PriorityQueue(count: 0): [] }
SelectableEventLoop { selector = Selector { descriptor = 7 }, scheduledTasks = PriorityQueue(count: 0): [] }
SelectableEventLoop { selector = Selector { descriptor = 8 }, scheduledTasks = PriorityQueue(count: 0): [] }
SelectableEventLoop { selector = Selector { descriptor = 9 }, scheduledTasks = PriorityQueue(count: 0): [] }
SelectableEventLoop { selector = Selector { descriptor = 10 }, scheduledTasks = PriorityQueue(count: 0): [] }
SelectableEventLoop { selector = Selector { descriptor = 3 }, scheduledTasks = PriorityQueue(count: 0): [] }
SelectableEventLoop { selector = Selector { descriptor = 4 }, scheduledTasks = PriorityQueue(count: 0): [] }
The description represents the id of the EventLoop. As you can see, you can use 8 different loops before being assigned again an existing EventLoop from the same group. As expected, this matches our number of cores.
Note: Under the hood, most EventLoops are designed using NIOThread
, so that the implementation can be cross-platform: NIO threads are build using Posix Threads. However, some platform specific loops, like NIO Transport service, are free from multiplatform constrains and are using Apple Dispatch library. It means, if you are targeting only MacOS, you can thus use SwiftNIO futures and promises directly with Dispatch library. Libdispatch being shipped with Swift on Linux now, it could also work there, but I did not test it yet.
Step 3: Executing async code
If you just want to execute async code without needing to wait back for a result, you can just pass a function closure to the EventLoop.execute(_:)
:
import NIO
print("System cores: \(System.coreCount)\n")
let evGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
let ev = evGroup.next()
ev.execute {
print("Hello, ")
}
// sleep(1)
print("world!")
try evGroup.syncShutdownGracefully()
In the previous code, the order in which “Hello, ” and “world!” are displayed is undetermined.
Still, on my computer, it is clear that they are not executed in order. The print-out in the execute block is run asynchronously, after the execution of the print-out in the main thread:
System cores: 8
world!
Hello,
You can uncomment the sleep(1)
function call to insert one second of delay before the second print-out instruction. It will “force” the ordering by delaying the main thread print-out and have “Hello, world!” be displayed in sequence.
Step 4: Waiting for async code execution
Adding timers in your code to order code execution is a very bad practice. If you want to wait for the async code execution, that’s where ‘futures’ and ‘promises’ comes into play.
The following code will submit an async code to run on an EventLoop. The asyncPrint
function will wait for a given delay in the EventLoop and then print the passed string.
When you call asyncPrint
, you get a promise in return. With that promise, you can call the method wait()
on it, to wait for the completion of the async code.
import NIO
// Async code
func asyncPrint(on ev: EventLoop, delayInSecond: Int, string: String) -> EventLoopFuture<Void> {
// Do the async work
let promise = ev.submit {
sleepAndPrint(delayInSecond: 1, string: string)
return
}
// Return the promise
return promise
}
func sleepAndPrint(delayInSecond: UInt32, string: String) {
sleep(delayInSecond)
print(string)
}
// ===========================
// Main program
print("System cores: \(System.coreCount)\n")
let evGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
let ev = evGroup.next()
let future = asyncPrint(on: ev, delayInSecond: 1, string: "Hello, ")
print("Waiting...")
try future.wait()
print("world!")
try evGroup.syncShutdownGracefully()
The print-out will pause for one second on the “Waiting…” message and then display the “Hello, ” and “world!” messages in order.
Step 5: Promises and futures result
When you need a result, you need to return a promise that will give you more than just a signaling letting you know the processing is done. Thus, it will not be a promise of a Void
result, but can return a more complex promise.
First, let’s see a promise of a simple result that cannot fail. In your async code, you can return a promise that will return the result of factorial calculation asynchronously. Your code will promise to return a Double
and then submit the job to the EventLoop.
import NIO
// Async code
func asyncFactorial(on ev: EventLoop, n: Double) -> EventLoopFuture<Double> {
// Do the async work
let promise = ev.submit { () -> Double in
return factorial(n: n)
}
// Return the promise
return promise
}
// I would use a BigInt library to go further small number factorial calculation
// but I do not want to introduce an external dependency.
func factorial(n: Double) -> Double {
if n >= 0 {
return n == 0 ? 1 : n * factorial(n: n - 1)
} else {
return 0 / 0
}
}
// ===========================
// Main program
print("System cores: \(System.coreCount)\n")
let evGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
let ev = evGroup.next()
let n: Double = 10
let future = asyncFactorial(on: ev, n: n)
print("Waiting...")
let result = try future.wait()
print("fact(\(n)) = \(result)")
try evGroup.syncShutdownGracefully()
The code will be executed asynchronously and the wait()
method will return the result:
System cores: 8
Waiting...
fact(10.0) = 3628800.0
Step 6: Success and error processing
If you are doing network operations, like downloading a web page for example, the operation can fail. You can thus handle more complex result, that can be either success
or error
. SwiftNIO offers a ready made type call ResultType
.
In the next example, we will show an async function performing an asynchronous network operation using callbacks and returning a future result of ResultType
. The ResultType
will wrap either the content of the downloaded page or a failure callback.
import NIO
import Foundation
// =============================================================================
// MARK: Helpers
struct CustomError: LocalizedError, CustomStringConvertible {
var title: String
var code: Int
var description: String { errorDescription() }
init(title: String?, code: Int) {
self.title = title ?? "Error"
self.code = code
}
func errorDescription() -> String {
"\(title) (\(code))"
}
}
// MARK: Async code
func asyncDownload(on ev: EventLoop, urlString: String) -> EventLoopFuture<String> {
// Prepare the promise
let promise = ev.makePromise(of: String.self)
// Do the async work
let url = URL(string: urlString)!
let task = URLSession.shared.dataTask(with: url) { data, response, error in
print("Task done")
if let error = error {
promise.fail(error)
return
}
if let httpResponse = response as? HTTPURLResponse {
if (200...299).contains(httpResponse.statusCode) {
if let mimeType = httpResponse.mimeType, mimeType == "text/html",
let data = data,
let string = String(data: data, encoding: .utf8) {
promise.succeed(string)
return
}
} else {
// TODO: Analyse response for better error handling
let httpError = CustomError(title: "HTTP error", code: httpResponse.statusCode)
promise.fail(httpError)
return
}
}
let err = CustomError(title: "no or invalid data returned", code: 0)
promise.fail(err)
}
task.resume()
// Return the promise of a future result
return promise.futureResult
}
// =============================================================================
// MARK: Main
print("System cores: \(System.coreCount)\n")
let evGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
let ev = evGroup.next()
print("Waiting...")
let future = asyncDownload(on: ev, urlString: "https://www.process-one.net/en/")
future.whenSuccess { page in
print("Page received")
}
future.whenFailure { error in
print("Error: \(error)")
}
// Timeout: As processing is async, we can handle timeout by just waiting in
// main thread before quitting.
// => Waiting 10 seconds for completion
sleep(10)
try evGroup.syncShutdownGracefully()
The previous code will either print “Page received” when the page is downloaded or print the error. As your success handler receives the page content itself, you could do something with it (print it, analyse it, etc.)
Step 7: Combining async work results
Where promises really shine is when you would like to chain several async calls that depend on each other. You can thus write a code that appear logically in a sequence, but that is actually asynchronous.
In the following code, we reuse the previous async download function and process several pages by counting the number of div
elements in all pages.
By wrapping this processing in a reduce function, we can download all web pages in parallel. We receive the page data as they are downloaded and we keep track of a counter of the number of div
per page. Finally, we return the total as the future result.
This is a more involved example that should give you a better taste of what developing with futures and promises looks like.
import NIO
import Foundation
// =============================================================================
// MARK: Helpers
struct CustomError: LocalizedError, CustomStringConvertible {
var title: String
var code: Int
var description: String { errorDescription() }
init(title: String?, code: Int) {
self.title = title ?? "Error"
self.code = code
}
func errorDescription() -> String {
"\(title) (\(code))"
}
}
// MARK: Async code
func asyncDownload(on ev: EventLoop, urlString: String) -> EventLoopFuture<String> {
// Prepare the promise
let promise = ev.makePromise(of: String.self)
// Do the async work
let url = URL(string: urlString)!
let task = URLSession.shared.dataTask(with: url) { data, response, error in
print("Loading \(url)")
if let error = error {
promise.fail(error)
return
}
if let httpResponse = response as? HTTPURLResponse {
if (200...299).contains(httpResponse.statusCode) {
if let mimeType = httpResponse.mimeType, mimeType == "text/html",
let data = data,
let string = String(data: data, encoding: .utf8) {
promise.succeed(string)
return
}
} else {
// TODO: Analyse response for better error handling
let httpError = CustomError(title: "HTTP error", code: httpResponse.statusCode)
promise.fail(httpError)
return
}
}
let err = CustomError(title: "no or invalid data returned", code: 0)
promise.fail(err)
}
task.resume()
// Return the promise of a future result
return promise.futureResult
}
// =============================================================================
// MARK: Main
print("System cores: \(System.coreCount)\n")
let evGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
var futures: [EventLoopFuture<String>] = []
for url in ["https://www.process-one.net/en/", "https://www.remond.im", "https://swift.org"] {
let ev = evGroup.next()
let future = asyncDownload(on: ev, urlString: url)
futures.append(future)
}
let futureResult = EventLoopFuture.reduce(0, futures, on: evGroup.next()) { (count: Int, page: String) -> Int in
let tok = page.components(separatedBy:"<div")
let p_count = tok.count-1
return count + p_count
}
futureResult.whenSuccess { count in
print("Result = \(count)")
}
futureResult.whenFailure { error in
print("Error: \(error)")
}
// Timeout: As processing is async, we can handle timeout by just waiting in
// main thread before quitting.
// => Waiting 10 seconds for completion
sleep(10)
try evGroup.syncShutdownGracefully()
This code actually builds a pipeline as follows:
Conclusion
Futures and promises are at the heart of SwiftNIO design. To better understand SwiftNIO architecture, you need to understand the futures and promises mechanism.
However, there is more concepts that you need to master to fully understand SwiftNIO. Most notably, inbound and outbound channels allow you to structure your networking code into reusable components executed in a pipeline.
I will cover more SwiftNIO concepts in a next blog post. In the meantime, please send us your feedback 😊