private Task WriteAsyncInternal(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.WriteAsync(array, offset, numBytes, cancellationToken);
}
if (!CanWrite) throw Error.GetWriteNotSupported();
Debug.Assert((_readPos == 0 && _readLength == 0 && _writePos >= 0) || (_writePos == 0 && _readPos <= _readLength), "We're either reading or writing, but not both.");
Debug.Assert(!_isPipe || (_readPos == 0 && _readLength == 0), "Win32FileStream must not have buffered data here! Pipes should be unidirectional.");
bool writeDataStoredInBuffer = false;
if (!_isPipe) // avoid async buffering with pipes, as doing so can lead to deadlocks (see comments in ReadInternalAsyncCore)
{
// Ensure the buffer is clear for writing
if (_writePos == 0)
{
if (_readPos < _readLength)
{
FlushReadBuffer();
}
_readPos = 0;
_readLength = 0;
}
// Determine how much space remains in the buffer
int remainingBuffer = _bufferLength - _writePos;
Debug.Assert(remainingBuffer >= 0);
// Simple/common case:
// - The write is smaller than our buffer, such that it's worth considering buffering it.
// - There's no active flush operation, such that we don't have to worry about the existing buffer being in use.
// - And the data we're trying to write fits in the buffer, meaning it wasn't already filled by previous writes.
// In that case, just store it in the buffer.
if (numBytes < _bufferLength && !HasActiveBufferOperation && numBytes <= remainingBuffer)
{
Buffer.BlockCopy(array, offset, GetBuffer(), _writePos, numBytes);
_writePos += numBytes;
writeDataStoredInBuffer = true;
// There is one special-but-common case, common because devs often use
// byte[] sizes that are powers of 2 and thus fit nicely into our buffer, which is
// also a power of 2. If after our write the buffer still has remaining space,
// then we're done and can return a completed task now. But if we filled the buffer
// completely, we want to do the asynchronous flush/write as part of this operation
// rather than waiting until the next write that fills the buffer.
if (numBytes != remainingBuffer)
return Task.CompletedTask;
Debug.Assert(_writePos == _bufferLength);
}
}
// At this point, at least one of the following is true:
// 1. There was an active flush operation (it could have completed by now, though).
// 2. The data doesn't fit in the remaining buffer (or it's a pipe and we chose not to try).
// 3. We wrote all of the data to the buffer, filling it.
//
// If there's an active operation, we can't touch the current buffer because it's in use.
// That gives us a choice: we can either allocate a new buffer, or we can skip the buffer
// entirely (even if the data would otherwise fit in it). For now, for simplicity, we do
// the latter; it could also have performance wins due to OS-level optimizations, and we could
// potentially add support for PreAllocatedOverlapped due to having a single buffer. (We can
// switch to allocating a new buffer, potentially experimenting with buffer pooling, should
// performance data suggest it's appropriate.)
//
// If the data doesn't fit in the remaining buffer, it could be because it's so large
// it's greater than the entire buffer size, in which case we'd always skip the buffer,
// or it could be because there's more data than just the space remaining. For the latter
// case, we need to issue an asynchronous write to flush that data, which then turns this into
// the first case above with an active operation.
//
// If we already stored the data, then we have nothing additional to write beyond what
// we need to flush.
//
// In any of these cases, we have the same outcome:
// - If there's data in the buffer, flush it by writing it out asynchronously.
// - Then, if there's any data to be written, issue a write for it concurrently.
// We return a Task that represents one or both.
// Flush the buffer asynchronously if there's anything to flush
Task flushTask = null;
if (_writePos > 0)
{
flushTask = FlushWriteAsync(cancellationToken);
// If we already copied all of the data into the buffer,
// simply return the flush task here. Same goes for if the task has
// already completed and was unsuccessful.
if (writeDataStoredInBuffer ||
flushTask.IsFaulted ||
flushTask.IsCanceled)
{
return flushTask;
}
}
Debug.Assert(!writeDataStoredInBuffer);
Debug.Assert(_writePos == 0);
// Finally, issue the write asynchronously, and return a Task that logically
// represents the write operation, including any flushing done.
Task writeTask = WriteInternalCoreAsync(array, offset, numBytes, cancellationToken);
return
(flushTask == null || flushTask.Status == TaskStatus.RanToCompletion) ? writeTask :
(writeTask.Status == TaskStatus.RanToCompletion) ? flushTask :
Task.WhenAll(flushTask, writeTask);
}