6.12.14 Non-Blocking I/O

Most ports in Guile are blocking: when you try to read a character from a port, Guile will block on the read until a character is ready, or end-of-stream is detected. Likewise whenever Guile goes to write (possibly buffered) data to an output port, Guile will block until all the data is written.

Interacting with ports in blocking mode is very convenient: you can write straightforward, sequential algorithms whose code flow reflects the flow of data. However, blocking I/O has two main limitations.

The first is that it’s easy to get into a situation where code is waiting on data. Time spent waiting on data when code could be doing something else is wasteful and prevents your program from reaching its peak throughput. If you implement a web server that sequentially handles requests from clients, it’s very easy for the server to end up waiting on a client to finish its HTTP request, or waiting on it to consume the response. The end result is that you are able to serve fewer requests per second than you’d like to serve.

The second limitation is related: a blocking parser over user-controlled input is a denial-of-service vulnerability. Indeed the so-called “slow loris” attack of the early 2010s was just that: an attack on common web servers that drip-fed HTTP requests, one character at a time. All it took was a handful of slow loris connections to occupy an entire web server.

In Guile we would like to preserve the ability to write straightforward blocking networking processes of all kinds, but under the hood to allow those processes to suspend their requests if they would block.

To do this, the first piece is to allow Guile ports to declare themselves as being nonblocking. This is currently supported only for file ports, which also includes sockets, terminals, or any other port that is backed by a file descriptor. To do that, we use an arcane UNIX incantation:

(let ((flags (fcntl socket F_GETFL)))
  (fcntl socket F_SETFL (logior O_NONBLOCK flags)))

Now the file descriptor is open in non-blocking mode. If Guile tries to read or write from this file and the read or write returns a result indicating that more data can only be had by doing a blocking read or write, Guile will block by polling on the socket’s read-wait-fd or write-wait-fd, to preserve the illusion of a blocking read or write. See Implementing New Port Types in C for more on those internal interfaces.

So far we have just reproduced the status quo: the file descriptor is non-blocking, but the operations on the port do block. To go farther, it would be nice if we could suspend the “thread” using delimited continuations, and only resume the thread once the file descriptor is readable or writable. (See Prompts).

But here we run into a difficulty. The ports code is implemented in C, which means that although we can suspend the computation to some outer prompt, we can’t resume it because Guile can’t resume delimited continuations that capture the C stack.

To overcome this difficulty we have created a compatible but entirely parallel implementation of port operations. To use this implementation, do the following:

(use-modules (ice-9 suspendable-ports))

This will replace the core I/O primitives like get-char and put-bytevector with new versions that are exactly the same as the ones in the standard library, but with two differences. One is that when a read or a write would block, the suspendable port operations call out the value of the current-read-waiter or current-write-waiter parameter, as appropriate. See Parameters. The default read and write waiters do the same thing that the C read and write waiters do, which is to poll. User code can parameterize the waiters, though, enabling the computation to suspend and allow the program to process other I/O operations. Because the new suspendable ports implementation is written in Scheme, that suspended computation can resume again later when it is able to make progress. Success!

The other main difference is that because the new ports implementation is written in Scheme, it is slower than C, currently by a factor of 3 or 4, though it depends on many factors. For this reason we have to keep the C implementations as the default ones. One day when Guile’s compiler is better, we can close this gap and have only one port operation implementation again.

Note that Guile does not currently include an implementation of the facility to suspend the current thread and schedule other threads in the meantime. Before adding such a thing, we want to make sure that we’re providing the right primitives that can be used to build schedulers and other user-space concurrency patterns, and that the patterns that we settle on are the right patterns. In the meantime, have a look at 8sync (https://gnu.org/software/8sync) for a prototype of an asynchronous I/O and concurrency facility.

Scheme Procedure: install-suspendable-ports!

Replace the core ports implementation with suspendable ports, as described above. This will mutate the values of the bindings like get-char, put-u8, and so on in place.

Scheme Procedure: uninstall-suspendable-ports!

Restore the original core ports implementation, un-doing the effect of install-suspendable-ports!.

Scheme Parameter: current-read-waiter
Scheme Parameter: current-write-waiter

Parameters whose values are procedures of one argument, called when a suspendable port operation would block on a port while reading or writing, respectively. The default values of these parameters do a blocking poll on the port’s file descriptor. The procedures are passed the port in question as their one argument.