在C#中将回调机制实现为async / await模式

时间:2015-07-23 21:22:02

标签: c# design-patterns asynchronous callback async-await

如何将以下回调驱动代码转换为异步/等待模式:

public class DeviceWrapper
{
 // external device which provides real time stream of data
 private InternalDevice device = new InternalDevice();
 private List<int> accumulationBuffer = new List<int>();

 public void StartReceiving()
 {
     // the following callback invocations might by synchronized by main
     // UI message pump, particular window message pump
     // or some other way
     device.Synchronization = Synchronization.UI;
     device.DataAvailable += DataAvailableHandler;
     device.ReceivingStoppedOrErrorOccured += StopHandler;
     device.Start();
 }

 private void DataAvailableHandler(object sender, DataEventArgs e)
 {
     // Filter data from e.Data and accumulate to accumulationBuffer field.
     // If certail condition is met, signal pending task (if there is any)
     //as complete return to the awaiting caller accumulationBuffer or perhaps temporary buffer created from accumulationBuffer
     // in order to make it available to the caller.
     // Handle also requested cancellation.
 }

 public Task<byte[]> GetData(CancellationToken token)
 {
     // create task returning data filtered and accumulated in DataAvailableHandler 
 }
}
// usage:
async void Test()
{
 DeviceWrapper w = new DeviceWrapper();
 w.StartReceiving();
 while(true)
 {
  byte[] filteredData = await w.GetData(CancellationToken.Null);
  Use(filteredData);
 }
}

我通过阅读.NET StreamReader类源寻求灵感来解决这个问题,但这让我更加困惑。

感谢专家的任何建议!

2 个答案:

答案 0 :(得分:3)

您正在寻找TaskCompletionSource<byte[]>。这是它看起来的近似值:

public Task<byte[]> GetData(CancellationToken token)
{
      cancellationToken.ThrowIfCancellationRequested;

      var tcs = new TaskCompletionSource<byte[]>();
      DataEventHandler dataHandler = null;
      dataHandler = (o, e) => 
      {
          device.DataAvailable -= dataHandler;
          tcs.SetResult(e.Data);
      }

      StopEventHandler stopHandler = null;
      stopHandler = (os, se) =>
      {
            device.ReceivingStoppedOrErrorOccured -= stopHandler;
            // Assuming stop handler has some sort of error property.
            tcs.SetException(se.Exception);
      }

      device.DataAvailable += dataHandler;
      device.ReceivingStoppedOrErrorOccured += stopHandler;
      device.Start();

      return tcs.Task;
}

答案 1 :(得分:1)

如果您正确使用异步等待,您的代码会更容易:

首先:

  • 如果你想调用异步功能,你应该自己异步
  • 每个异步函数都返回Task而不是void或Task <TResult&gt;而不是TResult
  • 有一个例外:异步事件处理程序可能返回void
  • 调用异步函数后,您可以执行其他操作,直到需要答案为止。但你不必做其他事情。
  • 一旦你需要答案等待任务,结果就是TResult。

现在实施你的例子。有几种方法可以解决这个问题,但我认为这通常是一个生产者 - 消费者模式:我们有一个对象,它以与另一个消耗它们的对象无关的速度生成事物。

您可以自己创建,使用信号量来发送新数据,但.NET已经有了这方面的内容:

System.Threading.Tasks.DataFlow.BufferBlock。

您需要下载微软nuget软件包。请参阅BufferBlock的MSDN描述中的备注。

BufferBlock是您发送类型为T的对象,而另一个任务则等待T类型的对象到达。完全支持async / await。

发件人方:

  • 缓冲区块实现ITargetBlock <T&gt;其中T是它发送的类型。
  • 您可以将类型为T的项目发送到任何ITargetBlock
  • 考虑使用ITargetBlock <T&gt;将发件人作为单独的对象。作为财产。
  • 每当有数据要分发时:发布它,或者如果你想使用async / await,则发送SendAsync。见后面的

消费者方:

  • BufferBlock <T&GT;实现als ISourceBlock <T&gt;
  • 使用者获取发件人将其对象发送到的ISourceBlock,在这种情况下是发件人使用的BufferBlock。
  • 启动时,使用者会使用Receive或ReceiveAsync等待数据到达。

好的,让我们把它们放在一起:

public class DeviceWrapper
{
    // external device which provides real time stream of data
    private InternalDevice device = new InternalDevice();
    // internal buffer replaced by the bufferBlock
    BufferBlock<byte> bufferBlock = new BufferBlock<byte>()

    public void StartReceiving() {...}

    private async void DataAvailableHandler(object sender, DataEventArgs e)
    {
        // get the input and convert it to a byte
        // post the byte to the buffer block asynchronously
        byte byteToSend = ...
        await this.bufferBlock.SendAsync(byteToSend);
    }

    public async Task<IEnumerable<byte>> GetData(CancellationToken token)
    {
        List<byte> receivedBytes = new List<byte>()
        while (await this.BufferBlock.OutputAvailableAsync(token))
        {   // a byte is available
            byte b = await this.bufferBlock.ReceiveAsync(token);
            receivedBytes.Add(b);
            if (receivedBytes.Count > ...)
            {
                return receivedBytes;
            }
            // else: not enough bytes received yet, wait for more
        }
    }
}   

async Task Test(CancellationToken token)
{
    DeviceWrapper w = new DeviceWrapper();
    w.StartReceiving();
    while(NoStopRequested)
    {
        token.ThrowIfCancellationrequested();
        var filteredData = await w.GetData(token);
        Use(filteredData);
    }
}

还有很多东西可以告诉BufferBlocks,特别是如果没有数据可用,如何整齐地停止它们MSDN有几个例子。 请参阅并行库中的DataFlow一章

https://msdn.microsoft.com/en-us/library/hh228603(v=vs.110).aspx