Threadsafe缓冲包装流

时间:2014-03-06 13:04:05

标签: c# .net multithreading concurrency buffer

我在SslStream之上使用TcpClient。不幸的是,`SslStream``不支持同时从多个线程写入或读取。这就是为什么我在它周围写了我自己的包装器:

private ConcurrentQueue<byte> sendQueue;

private volatile bool oSending;

private readonly object writeLock;


public async void Write(byte[] buffer, int offset, int count)
{
  if (oSending)
  {
    lock (writeLock)
    {
      foreach (var b in buffer)
      {
        sendQueue.Enqueue(b);
      }
    }
  }
  else
  {
    oSending = true;
    await stream.WriteAsync(buffer, offset, count);
    oSending = false;

    lock (writeLock)
    {
      if (sendQueue.Count > 0)
      {
        Write(sendQueue.ToArray(), 0, sendQueue.Count);
        sendQueue = new ConcurrentQueue<byte>();
      }
    }
  }
}

背后的意图如下:

  1. 如果流是空闲的,请立即写入流。
  2. 如果流忙,请写入缓冲区。
  3. 如果流从发送返回,请检查队列中是否有数据并以递归方式发送。
  4. 到目前为止,我已经尝试了几种解决方案,但似乎每次发送的数据都太多了。

    P.S。:我知道按字节顺序填充队列并不好,但这只是快速而又脏。

    更新:我根据Dirk的评论添加了队列删除。

3 个答案:

答案 0 :(得分:1)

  1. 您正在锁定对ConcurrentQueue<T>的访问权限 - 您不需要,该队列已经是线程安全的
  2. if(oSending) {} else {oSending = true}不是线程安全的。两个线程可能将oSending读为false,输入else块,并将其设置为true。现在你有两个线程写入流。
  3. 正如Dirk指出的那样,你不是要从队列中删除项目。

  4. 我的修改:

    1. 而不是使用布尔标志,而是使用Monitor.TryEnter来尝试访问流。如果当前正在写入流,则调用将立即返回 - 并继续写入缓冲区。

    2. 实施IDisposable并确保Dispose刷新缓冲区。

    3. 仅在写入队列时锁定队列,以维持字节顺序
    4. 将签名从async Task更改为async void

    5. private readonly ConcurrentQueue<byte> _bufferQueue = new ConcurrentQueue<byte>();
      
      private readonly object _bufferLock = new object();
      private readonly object _streamLock = new object();
      private readonly MemoryStream stream = new MemoryStream();
      
      public async Task Write(byte[] data, int offset, int count)
      {
          bool streamLockTaken = false;
      
          try
          {
              //attempt to acquire the lock - if lock is currently taken, return immediately
              Monitor.TryEnter(_streamLock, ref streamLockTaken);
      
              if (streamLockTaken) //write to stream
              {
                  //write data to stream and flush the buffer
                  await stream.WriteAsync(data, offset, count);
                  await FlushBuffer();
      
              }
              else //write to buffer
              {
                  lock (_bufferLock)
                      foreach (var b in data)
                          _bufferQueue.Enqueue(b);
              }
          }
          finally
          {
              if (streamLockTaken)
                  Monitor.Exit(_streamLock);
          }
      }
      
      private async Task FlushBuffer()
      {
          List<byte> bufferedData = new List<byte>();
          byte b;
          while (_bufferQueue.TryDequeue(out b))
              bufferedData.Add(b);
      
          await stream.WriteAsync(bufferedData.ToArray(), 0, bufferedData.Count);
      }
      
      public void Dispose()
      {
          lock(_streamLock)
              FlushBuffer().Wait();
      }
      

答案 1 :(得分:1)

<强>更新

使用TPL Dataflow

using System.Threading.Tasks.Dataflow;

public class DataflowStreamWriter
{
    private readonly MemoryStream _stream = new MemoryStream();
    private readonly ActionBlock<byte[]> _block;

    public DataflowStreamWriter()
    {
        _block = new ActionBlock<byte[]>(
                        bytes => _stream.Write(bytes, 0, bytes.Length));
    }

    public void Write(byte[] data)
    {
        _block.Post(data);
    }
}

这是一种更好的生产者 - 消费者方法。

每当有人将数据写入您的ConcurrentStreamWriter实例时,该数据将被添加到缓冲区。此方法是线程安全的,并且多个线程可能同时写入数据。这些是您的制作者

然后,您有一个使用者 - 使用缓冲区中的数据并将其写入流中。

BlockingCollection<T>用于在生产者和消费者之间进行通信。这样,如果没有人生产,消费者就会闲置。每当制作人开始并向缓冲区写入内容时,消费者就会醒来。

消费者被懒惰地初始化 - 当且仅当某些数据首次可用时才会创建。

public class ConcurrentStreamWriter : IDisposable
{
    private readonly MemoryStream _stream = new MemoryStream();
    private readonly BlockingCollection<byte> _buffer = new BlockingCollection<byte>(new ConcurrentQueue<byte>());

    private readonly object _writeBufferLock = new object();
    private Task _flusher;
    private volatile bool _disposed;

    private void FlushBuffer()
    {
        //keep writing to the stream, and block when the buffer is empty
        while (!_disposed)
            _stream.WriteByte(_buffer.Take());

        //when this instance has been disposed, flush any residue left in the ConcurrentStreamWriter and exit
        byte b;
        while (_buffer.TryTake(out b))
            _stream.WriteByte(b);
    }

    public void Write(byte[] data)
    {
        if (_disposed)
            throw new ObjectDisposedException("ConcurrentStreamWriter");

        lock (_writeBufferLock)
            foreach (var b in data)
                _buffer.Add(b);

        InitFlusher();
    }

    public void InitFlusher()
    {
        //safely create a new flusher task if one hasn't been created yet
        if (_flusher == null)
        {
            Task newFlusher = new Task(FlushBuffer);
            if (Interlocked.CompareExchange(ref _flusher, newFlusher, null) == null)
                newFlusher.Start();
        }
    }

    public void Dispose()
    {
        _disposed = true;
        if (_flusher != null)
            _flusher.Wait();

        _buffer.Dispose();
    }
}

答案 2 :(得分:0)

你不能只锁定底层流吗?我相信它可以这么简单:

private readonly object writeLock = new Object();

public async void Write(byte[] buffer, int offset, int count)
{
   lock (writeLock)
   {
      await stream.WriteAsync(buffer, offset, count);
   }
}

此外,随着您的排队实现,我认为有一个更改,写入可以排队机器人永远不会写入流。例如,在另一个线程已经出列但在该线程释放其锁定之前排队。