The GHC Commentary - Non-blocking I/O on Win32
This note discusses the implementation of non-blocking I/O on
Win32 platforms. It is not implemented yet (Apr 2002), but it seems worth
capturing the ideas. Thanks to Sigbjorn for writing them.
Background
GHC has provided non-blocking I/O support for Concurrent Haskell
threads on platforms that provide 'UNIX-style' non-blocking I/O for
quite a while. That is, platforms that let you alter the property of a
file descriptor to instead of having a thread block performing an I/O
operation that cannot be immediately satisfied, the operation returns
back a special error code (EWOULDBLOCK.) When that happens, the CH
thread that made the blocking I/O request is put into a blocked-on-IO
state (see Foreign.C.Error.throwErrnoIfRetryMayBlock). The RTS will
in a timely fashion check to see whether I/O is again possible
(via a call to select()), and if it is, unblock the thread & have it
re-try the I/O operation. The result is that other Concurrent Haskell
threads won't be affected, but can continue operating while a thread
is blocked on I/O.
Non-blocking I/O hasn't been supported by GHC on Win32 platforms, for
the simple reason that it doesn't provide the OS facilities described
above.
Win32 non-blocking I/O, attempt 1
Win32 does provide something select()-like, namely the
WaitForMultipleObjects() API. It takes an array of kernel object
handles plus a timeout interval, and waits for either one (or all) of
them to become 'signalled'. A handle representing an open file (for
reading) becomes signalled once there is input available.
So, it is possible to observe that I/O is possible using this
function, but not whether there's "enough" to satisfy the I/O request.
So, if we were to mimic select() usage with WaitForMultipleObjects(),
we'd correctly avoid blocking initially, but a thread may very well
block waiting for their I/O requests to be satisified once the file
handle has become signalled. [There is a fix for this -- only read
and write one byte at a the time -- but I'm not advocating that.]
Win32 non-blocking I/O, attempt 2
Asynchronous I/O on Win32 is supported via 'overlapped I/O'; that is,
asynchronous read and write requests can be made via the ReadFile() /
WriteFile () APIs, specifying position and length of the operation.
If the I/O requests cannot be handled right away, the APIs won't
block, but return immediately (and report ERROR_IO_PENDING as their
status code.)
The completion of the request can be reported in a number of ways:
- synchronously, by blocking inside Read/WriteFile(). (this is the
non-overlapped case, really.)
- as part of the overlapped I/O request, pass a HANDLE to an event
object. The I/O system will signal this event once the request
completed, which a waiting thread will then be able to see.
- by supplying a pointer to a completion routine, which will be
called as an Asynchronous Procedure Call (APC) whenever a thread
calls a select bunch of 'alertable' APIs.
- by associating the file handle with an I/O completion port. Once
the request completes, the thread servicing the I/O completion
port will be notified.
The use of I/O completion port looks the most interesting to GHC,
as it provides a central point where all I/O requests are reported.
Note: asynchronous I/O is only fully supported by OSes based on
the NT codebase, i.e., Win9x don't permit async I/O on files and
pipes. However, Win9x does support async socket operations, and
I'm currently guessing here, console I/O. In my view, it would
be acceptable to provide non-blocking I/O support for NT-based
OSes only.
Here's the design I currently have in mind:
- Upon startup, an RTS helper thread whose only purpose is to service
an I/O completion port, is created.
- All files are opened in 'overlapping' mode, and associated
with an I/O completion port.
- Overlapped I/O requests are used to implement read() and write().
- If the request cannot be satisified without blocking, the Haskell
thread is put on the blocked-on-I/O thread list & a re-schedule
is made.
- When the completion of a request is signalled via the I/O completion
port, the RTS helper thread will move the associated Haskell thread
from the blocked list onto the runnable list. (Clearly, care
is required here to have another OS thread mutate internal Scheduler
data structures.)
- In the event all Concurrent Haskell threads are blocked waiting on
I/O, the main RTS thread blocks waiting on an event synchronisation
object, which the helper thread will signal whenever it makes
a Haskell thread runnable.
I might do the communication between the RTS helper thread and the
main RTS thread differently though: rather than have the RTS helper
thread manipluate thread queues itself, thus requiring careful
locking, just have it change a bit on the relevant TSO, which the main
RTS thread can check at regular intervals (in some analog of
awaitEvent(), for example).
Last modified: Wed Aug 8 19:30:18 EST 2001