A while back I posted about using Overlapped I/O from the .NET Framework.  I've started integrating the hardware with the rest of the project and hit a snag.  It seems that if a thread makes an overlapped I/O request and later terminates, the I/O request is aborted and your IOCompletionCallback routine receives error code 995 (system error code ERROR_OPERATION_ABORTED, or System.IO.IOException): "The I/O operation has been aborted because of either a thread exit or an application request".  I haven't looked into why this happens, but functionally it seems that the Windoze kernel assumes that the I/O request is valid only as long as the requesting thread is alive and kicking, which seems both perfectly reasonable and unreasonable depending on your perspective.  If you do happen to know the specifics on the kernel's behavior here, please comment; you'll save me some digging.

Example

Here is a unit test that illustrates the problem; a brief walkthrough follows the code:

[Test]
public void OIOTerminatesOnThreadExit()
{
    TcpListener listener = new TcpListener( 8888 );
    TcpClient client = new TcpClient();

    Exception exception = null;

    Thread listeningThread = new Thread(
        delegate()
        {
            listener.Start();

            // block until we receive a client
            TcpClient myClient = listener.AcceptTcpClient();

            // initiate an overlapped I/O operation on
            //  the underlying socket
            myClient.GetStream().BeginRead(
                new byte[ 16 ], 0, 15,                    
                r => {
                    try
                    {
                        // calling EndRead should
                        //    yield an exception            
                        myClient.GetStream().EndRead( r );
                    }
                    catch( Exception e )
                    {
                        // save the exception for later
                        //  assertion and validation
                        exception = e;
                    }
                },
                null
            );
        }
    );

    // start the listening thread
    listeningThread.Start();

    // connect to the TcpListener, so it can initiate an
    //  overlapped I/O operation
    client.Connect( Dns.GetHostName(), 8888 );

    // wait for the listening thread to finish
    listeningThread.Join();

    // verify
    Assert.IsNotNull( exception );
    Assert.IsInstanceOfType( typeof( IOException ), exception );
    StringAssert.Contains(
        "The I/O operation has been aborted because of either a thread exit or an application request",
        exception.Message
    );
}

Note that for brevity this test contains no error handling or Tcp timeouts, which it really should.

The test creates a thread that starts a TcpListener and waits for a connection.  Once a connection is established the thread issues an overlapped I/O read request on the network stream.  The AsyncCallback handler for the BeginRead operation just calls EndRead, saving any exception that occurs for further scrutiny.  Immediately following the overlapped I/O request, the listeningThread terminates normally.

Once the listeningThread is started, the unit test uses a TcpClient to connect to the TcpListener.  This will allow the listeningThread to make the overlapped I/O request and terminate.  After the TcpClient is connected, the test waits for the listeningThread to terminate.  

At this point, the test verifies that an exception was received from the BeginRead AsyncRequest callback and validates its type and content.

Workaround

My current workaround is pretty simple: kick off the I/O operation from the thread pool instead of an application thread.  Thread pool threads don't really terminate like application threads, they just go back into the pool when their work unit is complete, and the kernel seems to be content to oblige I/O requests from the thread pool even after the thread is returned to the pool (e.g., when you call an asynchronous BeginRead operation from your AsyncResult callback).

Here's the example with the workaround applied:

[Test]
public void OIODoesNotTerminateOnThreadPoolThreadExit()
{
    TcpListener listener = new TcpListener( 8888 );
    TcpClient client = new TcpClient();

    Exception exception = null;

    // start the listeningThread on the thread pool
    ThreadPool.QueueUserWorkItem(
        delegate( object unused )
        {
            listener.Start();

            // block until we receive a client
            TcpClient myClient = listener.AcceptTcpClient();

            // initiate an overlapped I/O operation on
            //  the underlying socket
            myClient.GetStream().BeginRead(
                new byte[ 16 ], 0, 15,                    
                r => {
                    try
                    {
                        myClient.GetStream().EndRead( r );
                    }
                    catch( Exception e )
                    {
                        // save the exception for later
                        //  assertion and validation
                        exception = e;
                    }
                },
                null
            );
        }
    );

    // connect to the TcpListener, so it can initiate an
    //  overlapped I/O operation
    client.Connect( Dns.GetHostName(), 8888 );

    // verify
    Assert.IsNull( exception );            
}       

The only changes here are that the listening thread is queued on the thread pool, and the test verifies that the exception remains null.  Using the thread pool seems to counter the kernel behavior and keep the outstanding overlapped I/O requests active even after the thread work unit is complete.