SwiftNIO vs DispatchSource / other non-blocking IO

I understand the benefit of non-blocking IO, but I don't quit understand what SwiftNIO provides that other solutions don't. For instances, I wrote a network layer that uses sockets in non-blocking mode and DispatchSource to keep network IO from blocking a thread.

I started to look into converting a library I wrote (PG.swift) to SwiftNIO and the first thing many tutorials say to do is to call MultiThreadedEventLoopGroup(numThreads: System.coreCount). But if I do that for my library, and your HTTP server does that for their library, and so on and so on, wouldn't you be creating extra threads? Wouldn't it make more sense to share a thread pool like libDispatch does?

To be clear, I'm not trying to throw shade, just figure out the use case.

It makes a lot of sense to share an event loop group. We just had a discussion of this over here: How to wrap the initialization ceremony with a client.

Would it make sense to have a preconfigured default event loop group? Maybe (you might want to file an issue in GitHub). But you would still probably want to do the stuff outlined in the other thread (for testing and to avoid thread hopping).

P.S.: I also started a PG client for Noze.io a while back and considered converting that to a swift-nio-pg w/o extra dependencies. Looks like you are a lot further! Cool stuff.


@Helge_Hess1 has covered the event loop sharing question, so I'll cover the other parts.

Let's zoom out a bit.

Non-blocking I/O at the lowest layer available in a Unix OS is provided by libc in the BSD sockets API. Specifically, you can call fcntl with O_NONBLOCK and all of a sudden your socket uses non-blocking I/O.

However, for most applications this is a bad abstraction. Specifically, it is barely an abstraction at all. You need to worry about what you do with EAGAIN, how to manage send and receive buffers, how you wait for I/O status changes, how you propagate back pressure, and then also cover all of your application architectural concerns. It also provides no tooling for doing asynchronous file I/O. This makes writing applications at this level of abstraction onerous, and only worth doing if you have a specific behavioural or performance envelope you need to meet that higher levels of abstraction do not provide.

As a result, there are many higher-level abstractions for non-blocking I/O. In the Apple ecosystem there is DispatchIO and now Network.framework (DispatchSources is not strictly an asynchronous I/O framework, it's an eventing framework: you still need to work out how to do the I/O, and there is no requirement that you do it asynchonously). In the wider Unix ecosystem there is libev and libuv. Other programming languages have their own abstractions, such as Twisted/asyncio in Python, or the entire Node.js ecosystem.

Each of these abstractions works differently and provides different tools. These differences are like any other difference between things that do the same thing in different ways: they represent different target use cases, different development philosophies, and different areas of expertise.

Let's zoom back in again slightly and ask a more narrowly-focused question: what does SwiftNIO provide that is not provided by alternative solutions in the Swift space?

The main answer is that SwiftNIO isn't only a tool for doing the I/O, it's a framework for building an application. In that sense, DispatchSources or Network.framework are lower-level abstractions: they give you tools to perform the I/O, but they don't push you as far towards a certain structure for your application.

The structure we push towards is one that is extremely useful for server-side applications. It encourages small, reusable components with minimal shared state, communicating by message passing, and avoiding context switching and excessive synchronisation. This is focused very tightly on the use-case where your application is the most important piece of software running in user space, where it can assume that all of the spare system resources can be used by the application, and where there are a large number of connections that need to be parked and managed.

This set of requirements calls for a different abstraction than Dispatch provides, though with careful use Dispatch can be used to build up that abstraction (and indeed is when using NIOTransportServices). We need to give server users the maximum ability to configure their runtime, as they have a much better insight into their workloads than we do. This leads us to the explicit passing of EventLoopGroups, rather than Dispatch's ambient scheduling.

I hope that has helped clarify some of the decisions here.