Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

async void work around when 3rd party library uses it

Tags:

c#

async-await

Looking for help after searching has not produced a good suggestion.

I always avoid having async void methods in code. I don't use event handlers. Sometimes a vendor or library gives you no choice, and their methods are implemented as async void.

If my method itself returns Task, but i have no choice but to call a 3rd party library method with async void, is there a way to safely wrap their method in such a way that I can keep my code free of the async void dangers, as listed here about terminating my process?

StackOverflow - why is async void bad

An example of my concern is as follows: 3rd party library method looks like this

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

My method say on a service controller:

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

My method is fine except the 3rd party library is async void and throws an exception. My app is going to die. I don't want it to because my code is well written an not async void. But I can't control their code. Can i wrap the call to their async void method in such a way to protect my code from dying?

like image 446
Jason The Coder Avatar asked Nov 07 '22 08:11

Jason The Coder


1 Answers

It's tricky and it might not work for all scenarios, but it may be possible to track the life-time of an async void method, by starting its execution on a custom synchronization context. In this case, SynchronizationContext.OperationStarted / SynchronizationContext.OperationCompleted will be called upon start and end of the asynchronous void method, correspondingly.

In case an exception is thrown inside an async void method, it will be caught and re-thrown via SynchronizationContext.Post. Thus, it's also possible to collect all exceptions.

Below is an a complete console app example illustrating this approach, loosely based on Stephen Toub's AsyncPump (warning: only slightly tested):

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));
        }
    }
}
like image 158
noseratio Avatar answered Nov 15 '22 05:11

noseratio