private Task<int> ReadAsyncInternal(byte[] array, int offset, int numBytes, CancellationToken cancellationToken)
{
// If async IO is not supported on this platform or
// if this Win32FileStream was not opened with FileOptions.Asynchronous.
if (!_useAsyncIO)
{
return base.ReadAsync(array, offset, numBytes, cancellationToken);
}
if (!CanRead) throw Error.GetReadNotSupported();
Debug.Assert((_readPos == 0 && _readLength == 0 && _writePos >= 0) || (_writePos == 0 && _readPos <= _readLength), "We're either reading or writing, but not both.");
if (_isPipe)
{
// Pipes are tricky, at least when you have 2 different pipes
// that you want to use simultaneously. When redirecting stdout
// & stderr with the Process class, it's easy to deadlock your
// parent & child processes when doing writes 4K at a time. The
// OS appears to use a 4K buffer internally. If you write to a
// pipe that is full, you will block until someone read from
// that pipe. If you try reading from an empty pipe and
// Win32FileStream's ReadAsync blocks waiting for data to fill it's
// internal buffer, you will be blocked. In a case where a child
// process writes to stdout & stderr while a parent process tries
// reading from both, you can easily get into a deadlock here.
// To avoid this deadlock, don't buffer when doing async IO on
// pipes. But don't completely ignore buffered data either.
if (_readPos < _readLength)
{
int n = _readLength - _readPos;
if (n > numBytes) n = numBytes;
Buffer.BlockCopy(GetBuffer(), _readPos, array, offset, n);
_readPos += n;
// Return a completed task
return TaskFromResultOrCache(n);
}
else
{
Debug.Assert(_writePos == 0, "Win32FileStream must not have buffered write data here! Pipes should be unidirectional.");
return ReadNativeAsync(array, offset, numBytes, 0, cancellationToken);
}
}
Debug.Assert(!_isPipe, "Should not be a pipe.");
// Handle buffering.
if (_writePos > 0) FlushWriteBuffer();
if (_readPos == _readLength)
{
// I can't see how to handle buffering of async requests when
// filling the buffer asynchronously, without a lot of complexity.
// The problems I see are issuing an async read, we do an async
// read to fill the buffer, then someone issues another read
// (either synchronously or asynchronously) before the first one
// returns. This would involve some sort of complex buffer locking
// that we probably don't want to get into, at least not in V1.
// If we did a sync read to fill the buffer, we could avoid the
// problem, and any async read less than 64K gets turned into a
// synchronous read by NT anyways... --
if (numBytes < _bufferLength)
{
Task<int> readTask = ReadNativeAsync(GetBuffer(), 0, _bufferLength, 0, cancellationToken);
_readLength = readTask.GetAwaiter().GetResult();
int n = _readLength;
if (n > numBytes) n = numBytes;
Buffer.BlockCopy(GetBuffer(), 0, array, offset, n);
_readPos = n;
// Return a completed task (recycling the one above if possible)
return (_readLength == n ? readTask : TaskFromResultOrCache(n));
}
else
{
// Here we're making our position pointer inconsistent
// with our read buffer. Throw away the read buffer's contents.
_readPos = 0;
_readLength = 0;
return ReadNativeAsync(array, offset, numBytes, 0, cancellationToken);
}
}
else
{
int n = _readLength - _readPos;
if (n > numBytes) n = numBytes;
Buffer.BlockCopy(GetBuffer(), _readPos, array, offset, n);
_readPos += n;
if (n >= numBytes)
{
// Return a completed task
return TaskFromResultOrCache(n);
}
else
{
// For streams with no clear EOF like serial ports or pipes
// we cannot read more data without causing an app to block
// incorrectly. Pipes don't go down this path
// though. This code needs to be fixed.
// Throw away read buffer.
_readPos = 0;
_readLength = 0;
return ReadNativeAsync(array, offset + n, numBytes - n, n, cancellationToken);
}
}
}