Introduction
If you�re like me you�ve taken one look at the Microsoft documentation on overlapped I/O and concluded that you
don�t really need it. After all, for the vast majority of the applications I�ve had to work on, I/O processing goes something like this:
OpenFile()
While (File.HasData())
{
ReadData();
ProcessData();
WriteData();
}
CloseFile()
Typically you don�t need to setup I/O operations to allow processing and reading/writing to overlap.
Even if you did you could achieve similar results using multithreading. So why did Microsoft provide overlapping I/O? Well here�s one use for it.
Details
Let�s use named pipes instead of real files for our I/O. Let�s further assume we�re writing a server application, which
will handle one or more instances of a named pipe. (See Conclusions below for why I used a named pipe in this example).
This is one (greatly simplified and not error checked) way to write the loop.
I�m assuming a global BOOL
variable called gbl_bStop
initially set to
FALSE
.
while (!gbl_bStop)
{
HANDLE h = CreateNamedPipe();
if (ConnectNamedPipe(h))
_beginthread(threadProc, 0, h);
}
This loop runs until gbl_bStop
is set to TRUE
. It creates a named pipe and
then sits inside the ConnectNamedPipe()
call waiting for a client to connect. When a client connects the
ConnectNamedPipe()
call returns and a new thread is launched to handle this particular client/server connection.
The handle returned in the CreateNamedPipe()
call is passed to the thread procedure, which will presumably use
the handle for read or write operations.
Typically the above loop would be running in its own thread so that your application can handle other events.
This code works fine until you want to stop the server. The main thread sets gbl_bStop
to
TRUE
and waits in vain
for the thread to terminate. The problem is that very little of the threads time is spent executing code over which you have any control.
Most of the time it�s sitting inside that ConnectNamedPipe()
call, deep inside the Operating
System, waiting for client connections.
Aha, we can work around that. Let�s set gbl_bStop
to TRUE
and then make a connection (from our main thread) to
the named pipe. Well that will work but it also starts up yet another thread. Ok, we can work around
that by checking gbl_bStop
when we come out of the ConnectNamedPipe()
call to see if we should continue or exit.
Now our loop looks like this:
while (!gbl_bStop)
{
HANDLE h = CreateNamedPipe();
if (ConnectNamedPipe(h) && !gbl_bStop)
_beginthread(threadProc, 0, h);
}
Not much added code but don�t forget our shutdown routine also has to create a connection to the named pipe.
It�s getting somewhat complex, with code needed to shut down our application scattered all over the place.
Overlapped I/O usage
This is where overlapped I/O comes in. Let�s write our loop like this instead. I�m assuming a global EVENT object
called gbl_hStopEvent
and I�m still leaving out a lot of detail.
OVERLAPPED op;
HANDLE h,
handleArray[2];
BOOL bStop = FALSE;
memset(&op, 0, sizeof(op));
handleArray[0] = op.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
handleArray[1] = gbl_hStopEvent;
while (bStop == FALSE)
{
h = CreateNamedPipe(FILE_FLAG_OVERLAPPED);
ConnectNamedPipe(h, &op);
switch (WaitForMultipleObjects(2, handleArray, FALSE, INFINITE))
{
case WAIT_OBJECT_0:
_beginthread(threadProc, 0, h);
ResetEvent(handleArray[0]);
break;
case WAIT_OBJECT_0 + 1:
CloseHandle(h);
bStop = TRUE;
break;
}
}
CloseHandle(handleArray[0]);
The key to this is the WaitForMultipleObjects()
. We create an array containing a handle
to the global stop event and a handle to a local event. The local event handle is also used in the OVERLAPPED
structure.
We create the named pipe using overlapped I/O and then call ConnectNamedPipe()
. However, because we specified
overlapped I/O the Operating System returns control to us immediately and we fall into the WaitForMultipleObjects()
call.
If a client connects to the named pipe the OS will signal the event handle we passed in the OVERLAPPED
structure
and the code for WAIT_OBJECT_0
is executed. If, on the other hand, our main thread signals gbl_hStopEvent
the code
for WAIT_OBJECT_0 + 1
is executed and we will exit the loop entirely.
The same technique can be used for the thread procedure using the named pipe connection and for the client side code.
Conclusions
I chose named pipes for this example for simplicity (the examples require fewer lines of code compared to a socket example). But the choice of something other than a regular file on the disk was deliberate. When reading a disk file you're working with a known and predictable entity. On the large time scale (the time scale humans experience) a read from a disk file takes very little time and it's easy to determine when the operation has reached the end (end of file). Thus the pseudocode I showed at the start of the article is reasonable. But when dealing with interprocess communications across (possibly) network connections it's not possible to determine with any kind of accuracy just
when the operation might end.
An obvious (and wrong) solution might be to write a tight loop polling the named pipe for new data. But, given the asynchronous nature of a named pipe data conduit you run a risk of wasting an awful lot of CPU cycles executing that tight loop.
Overlapped I/O provides an easy way to ensure your application is always responsive to user input without consuming excessive amounts of CPU time. Code that doesn't run costs the least in performance.
History
- 19 December 2002 - First version.
- 15 January 2004 - Added some comments about why Named Pipes were used in the pseudocode.