当第三方库使用它时,async void可以解决

时间:2018-04-19 12:41:22

标签: c# async-await

搜索后寻求帮助并没有提出好的建议。

我总是避免在代码中使用异步void方法。我不使用事件处理程序。有时供应商或库无法做出选择,其方法也实现为async void。

如果我的方法本身返回Task,但我别无选择,只能使用async void调用第三方库方法,是否有办法安全地包装他们的方法,以便我可以保持我的代码不受异步如此列出的关于终止我的流程的无效危险?

StackOverflow - why is async void bad

我关注的一个例子如下: 第三方库方法看起来像这样

public async void GetSomethingFromAService()
{
    /// their implementation, and somewhere along the way it throws an       exception, in this async void method --- yuck for me
}

我的方法在服务控制器上说:

public async Task<bool> MyMethod()
{
   await ThirdPartyLibrary.GetSomethingFromAService();
   return await Task.FromResult(true);
}

我的方法很好,除了第三方库是异步void并抛出异常。我的应用程序即将死亡。我不希望它,因为我的代码编写得很好而不是异步void。但我无法控制他们的代码。我可以用这种方式将调用包装到异步void方法中,以保护我的代码免于死亡吗?

1 个答案:

答案 0 :(得分:1)

这很棘手,它可能不适用于所有场景,但可以通过在自定义同步上下文上开始执行来跟踪async void方法的生命周期。在这种情况下,相应地会在异步void方法的开始和结束时调用SynchronizationContext.OperationStarted / SynchronizationContext.OperationCompleted

如果在async void方法中抛出异常,它将被SynchronizationContext.Post捕获并重新抛出。因此,也可以收集所有例外情况。

下面是一个完整的控制台应用示例,说明了这种方法,基于Stephen Toub's AsyncPump(警告:仅经过轻微测试):

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace AsyncVoidTest
{
    class Program
    {
        static async void GetSomethingFromAService()
        {
            await Task.Delay(2000);
            throw new InvalidOperationException(nameof(GetSomethingFromAService));
        }

        static async Task<int> MyMethodAsync()
        {
            // call an ill-designed 3rd party async void method 
            // and await its completion
            var pump = new PumpingContext();
            var startingTask = pump.Run(GetSomethingFromAService);
            await Task.WhenAll(startingTask, pump.CompletionTask);
            return 42;
        }

        static async Task Main(string[] args)
        {
            try
            {
                await MyMethodAsync();
            }
            catch (Exception ex)
            {
                // this will catch the exception thrown from GetSomethingFromAService
                Console.WriteLine(ex);
            }
        }
    }

    /// <summary>
    /// PumpingContext, based on Stephen Toub's AsyncPump
    /// https://blogs.msdn.com/b/pfxteam/archive/2012/02/02/await-synchronizationcontext-and-console-apps-part-3.aspx
    /// https://stackoverflow.com/q/49921403/1768303
    /// </summary>
    internal class PumpingContext : SynchronizationContext
    {
        private int _pendingOps = 0;

        private readonly BlockingCollection<ValueTuple<SendOrPostCallback, object>> _callbacks =
            new BlockingCollection<ValueTuple<SendOrPostCallback, object>>();

        private readonly List<Exception> _exceptions = new List<Exception>();

        private TaskScheduler TaskScheduler { get; }

        public Task CompletionTask { get; }

        public PumpingContext(CancellationToken token = default(CancellationToken))
        {
            var taskSchedulerTcs = new TaskCompletionSource<TaskScheduler>();

            this.CompletionTask = Task.Run(() =>
            {
                SynchronizationContext.SetSynchronizationContext(this);
                taskSchedulerTcs.SetResult(TaskScheduler.FromCurrentSynchronizationContext());
                try
                {
                    // run a short-lived callback pumping loop on a pool thread
                    foreach (var callback in _callbacks.GetConsumingEnumerable(token))
                    {
                        try
                        {
                            callback.Item1.Invoke(callback.Item2);
                        }
                        catch (Exception ex)
                        {
                            _exceptions.Add(ex);
                        }
                    }
                }
                catch (Exception ex)
                {
                    _exceptions.Add(ex);
                }
                finally
                {
                    SynchronizationContext.SetSynchronizationContext(null);
                }
                if (_exceptions.Any())
                {
                    throw new AggregateException(_exceptions);
                }
            }, token);

            this.TaskScheduler = taskSchedulerTcs.Task.GetAwaiter().GetResult();
        }

        public Task Run(
            Action voidFunc,
            CancellationToken token = default(CancellationToken))
        {
            return Task.Factory.StartNew(() =>
            {
                OperationStarted();
                try
                {
                    voidFunc();
                }
                finally
                {
                    OperationCompleted();
                }
            }, token, TaskCreationOptions.None, this.TaskScheduler);
        }

        public Task<TResult> Run<TResult>(
            Func<Task<TResult>> taskFunc,
            CancellationToken token = default(CancellationToken))
        {
            return Task.Factory.StartNew<Task<TResult>>(async () =>
            {
                OperationStarted();
                try
                {
                    return await taskFunc();
                }
                finally
                {
                    OperationCompleted();
                }
            }, token, TaskCreationOptions.None, this.TaskScheduler).Unwrap();
        }

        // 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)
            {
                _callbacks.CompleteAdding();
            }
        }

        public override void Post(SendOrPostCallback d, object state)
        {
            _callbacks.Add((d, state));
        }

        public override void Send(SendOrPostCallback d, object state)
        {
            throw new NotImplementedException(nameof(Send));
        }
    }
}