【问题标题】:How can I preserve exception context in an async console application using AsyncPump?如何使用 AsyncPump 在异步控制台应用程序中保留异常上下文?
【发布时间】:2014-06-03 09:50:42
【问题描述】:

我正在使用Steven Toub's excellent AsyncPump class,它允许控制台应用程序使用 async/await 关键字。

但是,我有一个问题,代码中抛出的异常被泵捕获然后重新抛出,这导致原始调用堆栈和异常上下文丢失。

这是我的测试代码:

class Program
{
  static void Main(string[] arg)
  {
    AsyncPump.Run(() => MainAsync());
  }

  static async Task MainAsync()
  {
    throw new Exception(); // code should break here
  }
}

如果您运行此测试,调试器不会根据需要在 throw new Exception() 上中断。相反,它在 t.GetAwaiter().GetResult() 上中断,这是 AsyncPump 类本身的一部分。这使得调试应用程序变得非常困难。

有没有办法重新抛出异常,使调试器在原始位置中断,同时保留调用堆栈和上下文?

【问题讨论】:

    标签: c# exception-handling console async-await


    【解决方案1】:

    如果您对MainAsync 使用async void 签名,而不是async Task,您可能会看到所需的行为。这并不意味着您应该更改您的代码(async void 几乎从来都不是一个好主意),它只是意味着现有行为完全正常

    async Task 方法抛出的异常不会立即重新抛出。相反,它存储在Task 对象中(带有捕获的堆栈上下文),并且当通过task.Resulttask.Wait()await tasktask.GetAwaiter().GetResult() 观察到任务结果时将重新抛出。

    我发布了更详细的解释:TAP global exception handler

    附带说明,我使用了AsyncPump 的略微修改版本,它确保初始任务开始异步执行(即,在核心循环开始泵送之后),TaskScheduler.CurrentTaskScheduler.FromCurrentSynchronizationContext()

    /// <summary>
    /// PumpingSyncContext, based on AsyncPump
    /// http://blogs.msdn.com/b/pfxteam/archive/2012/02/02/await-synchronizationcontext-and-console-apps-part-3.aspx
    /// </summary>
    class PumpingSyncContext : SynchronizationContext
    {
        BlockingCollection<Action> _actions;
        int _pendingOps = 0;
    
        public TResult Run<TResult>(Func<Task<TResult>> taskFunc, CancellationToken token = default(CancellationToken))
        {
            _actions = new BlockingCollection<Action>();
            SynchronizationContext.SetSynchronizationContext(this);
            try
            {
                var scheduler = TaskScheduler.FromCurrentSynchronizationContext();
    
                var task = Task.Factory.StartNew(
                    async () =>
                    {
                        OperationStarted();
                        try
                        {
                            return await taskFunc();
                        }
                        finally
                        {
                            OperationCompleted();
                        }
                    },
                    token, TaskCreationOptions.None, scheduler).Unwrap();
    
                // pumping loop
                foreach (var action in _actions.GetConsumingEnumerable())
                    action();
    
                return task.GetAwaiter().GetResult();
            }
            finally
            {
                SynchronizationContext.SetSynchronizationContext(null);
            }
        }
    
        void Complete()
        {
            _actions.CompleteAdding();
        }
    
        // SynchronizationContext methods
        public override SynchronizationContext CreateCopy()
        {
            return this;
        }
    
        public override void OperationStarted()
        {
            // called when async void method is invoked 
            Interlocked.Increment(ref _pendingOps);
        }
    
        public override void OperationCompleted()
        {
            // called when async void method completes 
            if (Interlocked.Decrement(ref _pendingOps) == 0)
                Complete();
        }
    
        public override void Post(SendOrPostCallback d, object state)
        {
            _actions.Add(() => d(state));
        }
    
        public override void Send(SendOrPostCallback d, object state)
        {
            throw new NotImplementedException("Send");
        }
    }
    

    这部分也可以改:

    return task.GetAwaiter().GetResult();
    

    到这里:

    return task.Result;
    

    在这种情况下,异常将作为AggregateException 传播给调用者,AggregateException.InnerException 指向async 方法内部的原始异常。

    【讨论】:

    • 你有你的类 PumpingSyncContext 的使用例子吗?我试图用new PumpingSyncContext.Run&lt;bool&gt;(async () =&gt; {... return true;}) 替换我的AsyncPump.Run(async () =&gt; {}) 呼叫,但这导致我与SignalR 出现问题。我的客户等待的方法永远不会停止等待,尽管它确实在服务器上完成了。使用 AsyncPump 我没有这种行为。
    • 我找到了使用它的方法,但我想我可能做得不好。首先,我保存当前上下文。然后 A 创建一个新的 PumpingSyncContext 并将其设置为当前上下文。我在上面调用 Run(à 方法。一旦 Run() 完成,我就设置回初始上下文。问题这并没有解决我在客户端上的等待问题。为了能够“修复它”,我做了一个 @987654344 @ 作为 Func 中的第一个指令传递给 Run() 方法。为什么要修复它? ...
    • 可能与AspNetSynchronizationContext有关,在自定义之前激活:stackoverflow.com/q/23062154
    【解决方案2】:

    GetAwaiter().GetResult() 已经正确地重新抛出异常(假设您使用的是 .NET 4.5)。调用堆栈已正确保留。

    您观察到的是顶级异常的行为被捕获,并且 AFAIK 被 VS 严格视为同步的,没有办法影响它。听起来这会是一个很好的UserVoice item

    您可以选择breaking when an exception is thrown

    【讨论】:

    • 你说Ad-hoc message pumps do not work in every situation. E.g., any API that requires a particular SynchronizationContext will fail. Some classic ASP.NET APIs are like this; they silently deadlock if you use a custom SynchronizationContext. As another example, any method that runs on a UI thread and depends on STA COM marshalling would deadlock.
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-07-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-03-27
    相关资源
    最近更新 更多