5

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?

  • Can you show something about the usage of the method? It seems a bug to be fixed in the other library. – Patrick Hofman Apr 19 '18 at 12:48
  • 1
    It will depend entirely on whether the type provides any means of knowing when the operation has completed. If it's using some other form of asynchrony, such as an event based model, callbacks, etc., then you can use that to perform your continuations, if it provides no means of knowing when it's done, you...can't know when it's done. – Servy Apr 19 '18 at 13:09
  • 1
    You have no control over their bad design. Nothing you can do other than do not use it till get get it fixed. – Nkosi Apr 19 '18 at 13:27

1 Answers1

1

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));
        }
    }
}
noseratio
  • 59,932
  • 34
  • 208
  • 486