Future of Swift-NIO in light of Concurrency Roadmap

Indeed, custom executors will be really fantastic for SwiftNIO (and any other ecosystem that wants to do I/O with the OS interfaces and also benefit from async/await).

Let me quickly outline where we are:

This is now possible to play with, with the main snapshots available today. You'll need to apply this PR which adds async/await support for SwiftNIO.

This is the step that is not yet supported with today's snapshots but will (hopefully) be added with custom executors.

What this means is that in a program like this one (called NIOAsyncAwaitDemo)

let channel = try await makeHTTPChannel(host: "httpbin.org", port: 80)
print("OK, connected to \(channel)")

print("Sending request 1", terminator: "")
let response1 = try await channel.sendRequest(HTTPRequestHead(version: .http1_1,
                                                             method: .GET,
                                                             uri: "/base64/SGVsbG8gV29ybGQsIGZyb20gSFRUUEJpbiEgCg==",
                                                             headers: ["host": "httpbin.org"]))
print(", response:", String(buffer: response1.body ?? ByteBuffer()))

print("Sending request 2", terminator: "")
let response2 = try await channel.sendRequest(HTTPRequestHead(version: .http1_1,
                                                             method: .GET,
                                                             uri: "/get",
                                                             headers: ["host": "httpbin.org"]))
print(", response:", String(buffer: response2.body ?? ByteBuffer()))

try await channel.close()

We would need to do at least 6 thread switches:

  1. From the executor that runs the async code to NIO's I/O threads (EventLoop) to create the Channel (network connection)
  2. Back from NIO's I/O threads to the executor to report back the result (OK, or Error thrown)
  3. Back onto the I/O threads to send & receive the HTTP request/response
  4. Back onto the executor to report the result of the first HTTP request
  5. Once again, onto the I/O threads to send & receive the HTTP request/response
  6. And finally back once more to report the result of the second HTTP request

In reality, there will be more switches. With custom executors (which would mean that the EventLoop itself can become an executor) we could do the whole thing with 0 thread switches (like we can with the futures directly).

That would in theory be possible today although it would be really slow. Before custom executors, we wouldn't even just need to switch once per send/receive, we'd need to switch twice per "actor ChannelHandler" (in and out). With custom executors, this should become possible too. If that's sensible or not remains to be seen, it'll likely be a performance question because calling an async function is cheap but still more expensive than calling a synchronous function.

tl;dr: Custom executors will allow SwiftNIO (and any other system that needs to do I/O directly) to be fast with async/await because it doesn't force us to thread-switch all the time.

13 Likes