使用事件使用Dataflow转发生产者/消费者的异常

时间:2014-08-05 08:58:46

标签: c# concurrency async-await .net-4.5 tpl-dataflow

我正在尝试使用Dataflow实现针对Web服务的HTTP请求的生产者/消费者队列。我找到了an excellent post from Stephen Cleary,它正好涵盖了这个场景。但是,与Stephen的帖子相反,我无法将生产者队列标记为完整,因为客户端应该能够在应用程序的整个生命周期内将请求排队。这种方法背后的想法是客户端可以不断产生请求,并且如果有多个请求待处理(这是必需的),则消费者能够以不同方式处理请求。

此要求还导致在生产完成后无法启动请求的消耗,但必须在第一个请求入队时启动。这也要求我以非阻塞的方式开始消费(否则会导致死锁)。我通过异步调用完成了这个,这是不等待的,不幸的是阻碍了异常处理。在消费期间发生的异常(实施HTTP请求)不会冒泡,因为不等待消费函数的调用。我已经介绍了处理这类问题的事件,但这引出了以下问题:

  1. 使用事件将消费者的异常转发给生产者的客户是不是一个好主意?
  2. 对于我的用例,以这种方式实现生产者/消费者模式是一个好主意吗?
  3. 是否存在其他可能在特定情况下更有利的方法?
  4. 为了使它更明确,我准备了一个代码示例来说明我上面描述的问题:

    public class HttpConnector 
    {
        private BufferBlock<RequestPayload> queue;
    
        public delegate void ConnectorExceptionHandler(object sender, Exception e);
        public event ConnectorExceptionHandler ConnectorExceptionOccured;
    
        public Task<bool> ProduceRequest(RequestPayload payload)
        {
            if(this.queue == null)
            {
                this.queue = new BufferBlock<RequestPayload>();
                this.ConsumeRequestsAsync(queue);   //this call cannot be awaited since it would lead to a deadlock
                                                    //however, by not awaiting this call all exceptions raised in 
                                                    //ConsumeRequestsAsync will be lost
            }
            return await queue.SendAsync(payload)
        }
    
        public Task ConsumeRequestsAsync(BufferBlock<RequestPayload> queue)
        {
            while(await queue.OutputAvailableAsync())
            {
                try
                {
                    var payload = await queue.ReceiveAsync();
                    //do the HTTP request...
                }
                catch (Exception e)
                {
                    ConnectorExceptionOccured(this, e); //fire event to forward the exception to the client
                }
            }
        }
    }
    
    public class Client 
    {
        private HttpConnector connector = new HttpConnector();
    
        public Task<bool> UpdateDataAsync()
        {
            connector += (object sender, Exception e )  //register an event handler to receive exceptions occur 
                                                        //during the consumption of the requests 
            {
                //handle exception or re-throw 
            }; 
            connector.ProduceRequest(new RequestPayload());  //produce a request
        }
    }
    

1 个答案:

答案 0 :(得分:0)

通过事件转发异常有一些严重的缺点:

  • 无法进行自然异常处理。如果开发人员知道这种机制,他们就不会发现任何异常。
  • 在应用程序运行时期间,不能将AppDomain#UnhandledException 用于未处理的异常。事实上,如果您没有订阅“例外”事件,则例外将完全丢失。
  • 如果您只有一个要订阅的事件,那么您的异常对象需要大量的上下文信息才能找出导致异常的操作。

对于我们的问题,事实证明最好使用TaskCompletionSource,这是一种同步不同线程的标准技术。每个TaskCompletionSource对象提供RequestPayload类的实例。消费后,TaskCompletionSource.Task完成(结果或异常)。制作人不会返回queue.SendAsync(payload)payload.CompletionSource.Task的任务:

public class RequestPayload
{
    public IModelBase Payload { get; set; }
    public TaskCompletionSource<IResultBase> CompletionSource { get; private set; }
}

public class HttpConnector 
{
    private BufferBlock<RequestPayload> queue;

    public Task ProduceRequest(RequestPayload payload)
    {
        if(this.queue == null)
        {
            this.queue = new BufferBlock<RequestPayload>();
            this.ConsumeRequestsAsync(queue);  
        }
        await queue.SendAsync(payload);
        return await payload.CompletionSource.Task;

}

    public Task ConsumeRequestsAsync(BufferBlock<RequestPayload> queue)
    {
        while(await queue.OutputAvailableAsync())
        {
            try
            {
                 var payload = await queue.ReceiveAsync();
                 //do the HTTP request...
                 payload.CompletionSource.TrySetResult(null);
            }
            catch (Exception e)
            {
                 payload.CompletionSource.TrySetException(e)
            }
       }
   }
}

public class Client 
{
    private HttpConnector connector = new HttpConnector();

    public Task UpdateDataAsync()
    {
        try
        {
             await connector.ProduceRequest(new RequestPayload());
        } 
        catch(Exception e) { /*handle exception*/ }
    }
}