Implementing async stream for producer/cosumer in C# / .NET

后端 未结 3 1526
感情败类
感情败类 2020-12-08 23:52

There is a lib that outputs its results into a given Stream object. I would like to begin consuming the results before the lib is done. The Stream should be blocking to simp

3条回答
  •  無奈伤痛
    2020-12-09 00:21

    I used the Yuric BlockingStream for a bit until performance dropped dramatically after running for 20 minutes to an hour in our code. I believe the performance drop was due to the garbage collector and the plethora of buffers created in that method when using it to stream a lot of data quickly (I didn't have time to prove it). I ended up creating a ring buffer version which doesn't suffer from performance degradation when used with our code.

    /// 
    /// A ring-buffer stream that you can read from and write to from
    /// different threads.
    /// 
    public class RingBufferedStream : Stream
    {
        private readonly byte[] store;
    
        private readonly ManualResetEventAsync writeAvailable
            = new ManualResetEventAsync(false);
    
        private readonly ManualResetEventAsync readAvailable
            = new ManualResetEventAsync(false);
    
        private readonly CancellationTokenSource cancellationTokenSource
            = new CancellationTokenSource();
    
        private int readPos;
    
        private int readAvailableByteCount;
    
        private int writePos;
    
        private int writeAvailableByteCount;
    
        private bool disposed;
    
        /// 
        /// Initializes a new instance of the 
        /// class.
        /// 
        /// 
        /// The maximum number of bytes to buffer.
        /// 
        public RingBufferedStream(int bufferSize)
        {
            this.store = new byte[bufferSize];
            this.writeAvailableByteCount = bufferSize;
            this.readAvailableByteCount = 0;
        }
    
        /// 
        public override bool CanRead => true;
    
        /// 
        public override bool CanSeek => false;
    
        /// 
        public override bool CanWrite => true;
    
        /// 
        public override long Length
        {
            get
            {
                throw new NotSupportedException(
                    "Cannot get length on RingBufferedStream");
            }
        }
    
        /// 
        public override int ReadTimeout { get; set; } = Timeout.Infinite;
    
        /// 
        public override int WriteTimeout { get; set; } = Timeout.Infinite;
    
        /// 
        public override long Position
        {
            get
            {
                throw new NotSupportedException(
                    "Cannot set position on RingBufferedStream");
            }
    
            set
            {
                throw new NotSupportedException(
                    "Cannot set position on RingBufferedStream");
            }
        }
    
        /// 
        /// Gets the number of bytes currently buffered.
        /// 
        public int BufferedByteCount => this.readAvailableByteCount;
    
        /// 
        public override void Flush()
        {
            // nothing to do
        }
    
        /// 
        /// Set the length of the current stream. Always throws .
        /// 
        /// 
        /// The desired length of the current stream in bytes.
        /// 
        public override void SetLength(long value)
        {
            throw new NotSupportedException(
                "Cannot set length on RingBufferedStream");
        }
    
        /// 
        /// Sets the position in the current stream. Always throws .
        /// 
        /// 
        /// The byte offset to the  parameter.
        /// 
        /// 
        /// A value of type  indicating the reference
        /// point used to obtain the new position.
        /// 
        /// 
        /// The new position within the current stream.
        /// 
        public override long Seek(long offset, SeekOrigin origin)
        {
            throw new NotSupportedException("Cannot seek on RingBufferedStream");
        }
    
        /// 
        public override void Write(byte[] buffer, int offset, int count)
        {
            if (this.disposed)
            {
                throw new ObjectDisposedException("RingBufferedStream");
            }
    
            Monitor.Enter(this.store);
            bool haveLock = true;
            try
            {
                while (count > 0)
                {
                    if (this.writeAvailableByteCount == 0)
                    {
                        this.writeAvailable.Reset();
                        Monitor.Exit(this.store);
                        haveLock = false;
                        bool canceled;
                        if (!this.writeAvailable.Wait(
                            this.WriteTimeout,
                            this.cancellationTokenSource.Token,
                            out canceled) || canceled)
                        {
                            break;
                        }
    
                        Monitor.Enter(this.store);
                        haveLock = true;
                    }
                    else
                    {
                        var toWrite = this.store.Length - this.writePos;
                        if (toWrite > this.writeAvailableByteCount)
                        {
                            toWrite = this.writeAvailableByteCount;
                        }
    
                        if (toWrite > count)
                        {
                            toWrite = count;
                        }
    
                        Array.Copy(
                            buffer,
                            offset,
                            this.store,
                            this.writePos,
                            toWrite);
                        offset += toWrite;
                        count -= toWrite;
                        this.writeAvailableByteCount -= toWrite;
                        this.readAvailableByteCount += toWrite;
                        this.writePos += toWrite;
                        if (this.writePos == this.store.Length)
                        {
                            this.writePos = 0;
                        }
    
                        this.readAvailable.Set();
                    }
                }
            }
            finally
            {
                if (haveLock)
                {
                    Monitor.Exit(this.store);
                }
            }
        }
    
        /// 
        public override void WriteByte(byte value)
        {
            if (this.disposed)
            {
                throw new ObjectDisposedException("RingBufferedStream");
            }
    
            Monitor.Enter(this.store);
            bool haveLock = true;
            try
            {
                while (true)
                {
                    if (this.writeAvailableByteCount == 0)
                    {
                        this.writeAvailable.Reset();
                        Monitor.Exit(this.store);
                        haveLock = false;
                        bool canceled;
                        if (!this.writeAvailable.Wait(
                            this.WriteTimeout,
                            this.cancellationTokenSource.Token,
                            out canceled) || canceled)
                        {
                            break;
                        }
    
                        Monitor.Enter(this.store);
                        haveLock = true;
                    }
                    else
                    {
                        this.store[this.writePos] = value;
                        --this.writeAvailableByteCount;
                        ++this.readAvailableByteCount;
                        ++this.writePos;
                        if (this.writePos == this.store.Length)
                        {
                            this.writePos = 0;
                        }
    
                        this.readAvailable.Set();
                        break;
                    }
                }
            }
            finally
            {
                if (haveLock)
                {
                    Monitor.Exit(this.store);
                }
            }
        }
    
        /// 
        public override int Read(byte[] buffer, int offset, int count)
        {
            if (this.disposed)
            {
                throw new ObjectDisposedException("RingBufferedStream");
            }
    
            Monitor.Enter(this.store);
            int ret = 0;
            bool haveLock = true;
            try
            {
                while (count > 0)
                {
                    if (this.readAvailableByteCount == 0)
                    {
                        this.readAvailable.Reset();
                        Monitor.Exit(this.store);
                        haveLock = false;
                        bool canceled;
                        if (!this.readAvailable.Wait(
                            this.ReadTimeout,
                            this.cancellationTokenSource.Token,
                            out canceled) || canceled)
                        {
                            break;
                        }
    
                        Monitor.Enter(this.store);
                        haveLock = true;
                    }
                    else
                    {
                        var toRead = this.store.Length - this.readPos;
                        if (toRead > this.readAvailableByteCount)
                        {
                            toRead = this.readAvailableByteCount;
                        }
    
                        if (toRead > count)
                        {
                            toRead = count;
                        }
    
                        Array.Copy(
                            this.store,
                            this.readPos,
                            buffer,
                            offset,
                            toRead);
                        offset += toRead;
                        count -= toRead;
                        this.readAvailableByteCount -= toRead;
                        this.writeAvailableByteCount += toRead;
                        ret += toRead;
                        this.readPos += toRead;
                        if (this.readPos == this.store.Length)
                        {
                            this.readPos = 0;
                        }
    
                        this.writeAvailable.Set();
                    }
                }
            }
            finally
            {
                if (haveLock)
                {
                    Monitor.Exit(this.store);
                }
            }
    
            return ret;
        }
    
        /// 
        public override int ReadByte()
        {
            if (this.disposed)
            {
                throw new ObjectDisposedException("RingBufferedStream");
            }
    
            Monitor.Enter(this.store);
            int ret = -1;
            bool haveLock = true;
            try
            {
                while (true)
                {
                    if (this.readAvailableByteCount == 0)
                    {
                        this.readAvailable.Reset();
                        Monitor.Exit(this.store);
                        haveLock = false;
                        bool canceled;
                        if (!this.readAvailable.Wait(
                            this.ReadTimeout,
                            this.cancellationTokenSource.Token,
                            out canceled) || canceled)
                        {
                            break;
                        }
    
                        Monitor.Enter(this.store);
                        haveLock = true;
                    }
                    else
                    {
                        ret = this.store[this.readPos];
                        ++this.writeAvailableByteCount;
                        --this.readAvailableByteCount;
                        ++this.readPos;
                        if (this.readPos == this.store.Length)
                        {
                            this.readPos = 0;
                        }
    
                        this.writeAvailable.Set();
                        break;
                    }
                }
            }
            finally
            {
                if (haveLock)
                {
                    Monitor.Exit(this.store);
                }
            }
    
            return ret;
        }
    
        /// 
        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                this.disposed = true;
                this.cancellationTokenSource.Cancel();
            }
    
            base.Dispose(disposing);
        }
    }
    

    That class uses our ManualResetEventAsync to help with clean closing.

    /// 
    ///     Asynchronous version of 
    /// 
    public sealed class ManualResetEventAsync
    {
        /// 
        /// The task completion source.
        /// 
        private volatile TaskCompletionSource taskCompletionSource =
            new TaskCompletionSource();
    
        /// 
        /// Initializes a new instance of the 
        /// class with a  value indicating whether to set the
        /// initial state to signaled.
        /// 
        /// 
        /// True to set the initial state to signaled; false to set the initial
        /// state to non-signaled.
        /// 
        public ManualResetEventAsync(bool initialState)
        {
            if (initialState)
            {
                this.Set();
            }
        }
    
        /// 
        /// Return a task that can be consumed by 
        /// 
        /// 
        /// The asynchronous waiter.
        /// 
        public Task GetWaitTask()
        {
            return this.taskCompletionSource.Task;
        }
    
        /// 
        /// Mark the event as signaled.
        /// 
        public void Set()
        {
            var tcs = this.taskCompletionSource;
            Task.Factory.StartNew(
                s => ((TaskCompletionSource)s).TrySetResult(true),
                tcs,
                CancellationToken.None,
                TaskCreationOptions.PreferFairness,
                TaskScheduler.Default);
            tcs.Task.Wait();
        }
    
        /// 
        /// Mark the event as not signaled.
        /// 
        public void Reset()
        {
            while (true)
            {
                var tcs = this.taskCompletionSource;
                if (!tcs.Task.IsCompleted
    #pragma warning disable 420
                    || Interlocked.CompareExchange(
                        ref this.taskCompletionSource,
                        new TaskCompletionSource(),
                        tcs) == tcs)
    #pragma warning restore 420
                {
                    return;
                }
            }
        }
    
        /// 
        /// Waits for the  to be signaled.
        /// 
        /// 
        /// The  waiting 
        /// was canceled -or- an exception was thrown during the execution
        /// of the  waiting .
        /// 
        public void Wait()
        {
            this.GetWaitTask().Wait();
        }
    
        /// 
        /// Waits for the  to be signaled.
        /// 
        /// 
        /// A  to observe while waiting for
        /// the task to complete.
        /// 
        /// 
        /// The  was canceled.
        /// 
        /// 
        /// The  waiting  was
        /// canceled -or- an exception was thrown during the execution of the
        ///  waiting .
        /// 
        public void Wait(CancellationToken cancellationToken)
        {
            this.GetWaitTask().Wait(cancellationToken);
        }
    
        /// 
        /// Waits for the  to be signaled.
        /// 
        /// 
        /// A  to observe while waiting for
        /// the task to complete.
        /// 
        /// 
        /// Set to true if the wait was canceled via the .
        /// 
        public void Wait(CancellationToken cancellationToken, out bool canceled)
        {
            try
            {
                this.GetWaitTask().Wait(cancellationToken);
                canceled = false;
            }
            catch (Exception ex)
                when (ex is OperationCanceledException
                    || (ex is AggregateException
                        && ex.InnerOf() != null))
            {
                canceled = true;
            }
        }
    
        /// 
        /// Waits for the  to be signaled.
        /// 
        /// 
        /// A  that represents the number of
        /// milliseconds to wait, or a  that
        /// represents -1 milliseconds to wait indefinitely.
        /// 
        /// 
        /// true if the  was signaled within
        /// the allotted time; otherwise, false.
        /// 
        /// 
        ///  is a negative number other than -1
        /// milliseconds, which represents an infinite time-out -or-
        /// timeout is greater than .
        /// 
        public bool Wait(TimeSpan timeout)
        {
            return this.GetWaitTask().Wait(timeout);
        }
    
        /// 
        /// Waits for the  to be signaled.
        /// 
        /// 
        /// The number of milliseconds to wait, or
        ///  (-1) to wait
        /// indefinitely.
        /// 
        /// 
        /// true if the  was signaled within
        /// the allotted time; otherwise, false.
        /// 
        /// 
        ///  is a negative number other
        /// than -1, which represents an infinite time-out.
        /// 
        public bool Wait(int millisecondsTimeout)
        {
            return this.GetWaitTask().Wait(millisecondsTimeout);
        }
    
        /// 
        /// Waits for the  to be signaled.
        /// 
        /// 
        /// The number of milliseconds to wait, or
        ///  (-1) to wait
        /// indefinitely.
        /// 
        /// 
        /// A  to observe while waiting for the
        ///  to be signaled.
        /// 
        /// 
        /// true if the  was signaled within
        /// the allotted time; otherwise, false.
        /// 
        /// 
        /// The  waiting 
        /// was canceled -or- an exception was thrown during the execution of
        /// the  waiting .
        /// 
        /// 
        ///  is a negative number other
        /// than -1, which represents an infinite time-out.
        /// 
        /// 
        /// The  was canceled.
        /// 
        public bool Wait(int millisecondsTimeout, CancellationToken cancellationToken)
        {
            return this.GetWaitTask().Wait(millisecondsTimeout, cancellationToken);
        }
    
        /// 
        /// Waits for the  to be signaled.
        /// 
        /// 
        /// The number of milliseconds to wait, or
        ///  (-1) to wait
        /// indefinitely.
        /// 
        /// 
        /// A  to observe while waiting for the
        ///  to be signaled.
        /// 
        /// 
        /// Set to true if the wait was canceled via the .
        /// 
        /// 
        /// true if the  was signaled within
        /// the allotted time; otherwise, false.
        /// 
        /// 
        ///  is a negative number other
        /// than -1, which represents an infinite time-out.
        /// 
        public bool Wait(
            int millisecondsTimeout,
            CancellationToken cancellationToken,
            out bool canceled)
        {
            bool ret = false;
            try
            {
                ret = this.GetWaitTask().Wait(millisecondsTimeout, cancellationToken);
                canceled = false;
            }
            catch (Exception ex)
                when (ex is OperationCanceledException
                    || (ex is AggregateException
                        && ex.InnerOf() != null))
            {
                canceled = true;
            }
    
            return ret;
        }
    }
    

    And, ManualResetEventAsync uses the InnerOf extension...

    /// 
    ///     Extension functions.
    /// 
    public static class Extensions
    {
        /// 
        /// Finds the first exception of the requested type.
        /// 
        /// 
        /// The type of exception to return
        /// 
        /// 
        /// The exception to look in.
        /// 
        /// 
        /// The exception or the first inner exception that matches the
        /// given type; null if not found.
        /// 
        public static T InnerOf(this Exception ex)
            where T : Exception
        {
            return (T)InnerOf(ex, typeof(T));
        }
    
        /// 
        /// Finds the first exception of the requested type.
        /// 
        /// 
        /// The exception to look in.
        /// 
        /// 
        /// The type of exception to return
        /// 
        /// 
        /// The exception or the first inner exception that matches the
        /// given type; null if not found.
        /// 
        public static Exception InnerOf(this Exception ex, Type t)
        {
            if (ex == null || t.IsInstanceOfType(ex))
            {
                return ex;
            }
    
            var ae = ex as AggregateException;
            if (ae != null)
            {
                foreach (var e in ae.InnerExceptions)
                {
                    var ret = InnerOf(e, t);
                    if (ret != null)
                    {
                        return ret;
                    }
                }
            }
    
            return InnerOf(ex.InnerException, t);
        }
    }
    

提交回复
热议问题