/* OMG THIS CODE IS COMPLICATED * * Here's the core idea. We want to create a ReadAsync function that * reads from our list of byte arrays **until it gets to the end of * our current list**. * * If we're not there yet, we keep returning data, serializing access * to the underlying position pointer (i.e. we definitely don't want * people concurrently moving position along). If we try to read past * the end, we return the section of data we could read and complete * it. * * Here's where the tricky part comes in. If we're not Completed (i.e. * the caller still wants to add more byte arrays in the future) and * we're at the end of the current stream, we want to *block* the read * (not blocking, but async blocking whatever you know what I mean), * until somebody adds another byte[] to chew through, or if someone * rewinds the position. * * If we *are* completed, we should return zero to simply complete the * read, signalling we're at the end of the stream */ public override async Task <int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { retry: int bytesRead = 0; int buffersToRemove = 0; if (isCompleted && position == maxLength) { return(0); } if (exception != null) { throw exception; } using (await readStreamLock.LockAsync().ConfigureAwait(false)) { lock (bytes) { foreach (var buf in bytes) { cancellationToken.ThrowIfCancellationRequested(); if (exception != null) { throw exception; } int toCopy = Math.Min(count, buf.Length - offsetInCurrentBuffer); Array.ConstrainedCopy(buf, offsetInCurrentBuffer, buffer, offset, toCopy); count -= toCopy; offset += toCopy; bytesRead += toCopy; offsetInCurrentBuffer += toCopy; if (offsetInCurrentBuffer >= buf.Length) { offsetInCurrentBuffer = 0; buffersToRemove++; } if (count <= 0) { break; } } // Remove buffers that we read in this operation bytes.RemoveRange(0, buffersToRemove); position += bytesRead; } } // If we're at the end of the stream and it's not done, prepare // the next read to park itself unless AddByteArray or Complete // posts if (position >= maxLength && !isCompleted) { lockRelease = await readStreamLock.LockAsync().ConfigureAwait(false); } if (bytesRead == 0 && !isCompleted) { // NB: There are certain race conditions where we somehow acquire // the lock yet are at the end of the stream, and we're not completed // yet. We should try again so that we can get stuck in the lock. goto retry; } if (cancellationToken.IsCancellationRequested) { Interlocked.Exchange(ref lockRelease, EmptyDisposable.Instance).Dispose(); cancellationToken.ThrowIfCancellationRequested(); } if (exception != null) { Interlocked.Exchange(ref lockRelease, EmptyDisposable.Instance).Dispose(); throw exception; } if (isCompleted && position < maxLength) { // NB: This solves a rare deadlock // // 1. ReadAsync called (waiting for lock release) // 2. AddByteArray called (release lock) // 3. AddByteArray called (release lock) // 4. Complete called (release lock the last time) // 5. ReadAsync called (lock released at this point, the method completed successfully) // 6. ReadAsync called (deadlock on LockAsync(), because the lock is block, and there is no way to release it) // // Current condition forces the lock to be released in the end of 5th point Interlocked.Exchange(ref lockRelease, EmptyDisposable.Instance).Dispose(); } return(bytesRead); }
public ByteArrayListStream() { // Initially we have nothing to read so Reads should be parked readStreamLock.LockAsync().ContinueWith(t => lockRelease = t.Result); }