How can I preserve exception context in an async console application using AsyncPump?

后端 未结 2 1327
心在旅途
心在旅途 2021-01-06 07:25

I am using Steven Toub\'s excellent AsyncPump class that allows console applications to use the async/await keywords.

However, I have a problem where exceptions that

2条回答
  •  太阳男子
    2021-01-06 08:00

    You would probably see the desired behavior if you used async void signature for MainAsync, rather than async Task. This doesn't mean you should change your code (async void is almost never a good idea), it just means that the existing behavior is perfectly normal.

    An exception thrown from async Task methods is not re-thrown immediately. Rather, it is stored inside the Task object (with the captured stack context) and will be re-thrown when the task's result gets observed via task.Result, task.Wait(), await task or task.GetAwaiter().GetResult().

    I posted a bit more detailed explanation of this: TAP global exception handler.

    On a side note, I use a slightly modified version of AsyncPump, which makes sure the initial task starts executing asynchronously (i.e., after the core loop has started pumping), with TaskScheduler.Current being TaskScheduler.FromCurrentSynchronizationContext():

    /// 
    /// PumpingSyncContext, based on AsyncPump
    /// http://blogs.msdn.com/b/pfxteam/archive/2012/02/02/await-synchronizationcontext-and-console-apps-part-3.aspx
    /// 
    class PumpingSyncContext : SynchronizationContext
    {
        BlockingCollection _actions;
        int _pendingOps = 0;
    
        public TResult Run(Func> taskFunc, CancellationToken token = default(CancellationToken))
        {
            _actions = new BlockingCollection();
            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");
        }
    }
    

    It's also possible to change this part:

    return task.GetAwaiter().GetResult();
    

    To this:

    return task.Result;
    

    In this case, the exception will be propagated to the caller as AggregateException, with AggregateException.InnerException pointing to the original exception from inside the async method.

提交回复
热议问题