Ожидание завершения async void возможно, но легально ли это?

Дисклеймер: велосипед изобрел не я, а разработчики ASP.NET

Не так давно наткнулся на статью от S. Cleary про варианты решения проблем при инвоке событий, в которых используются async void обработчики, и от них же требуется обратная связь через аргументы события.

И меня до жути заинтересовала часть про контекст синхронизации, и я решил проверить, как это работает. Создал вот такой контекст синхронизации.

public class MyContext : SynchronizationContext, IAsyncDisposable
{
    private SynchronizationContext _context;
    private TaskCompletionSource _src;
    private int count;
    private Task _task;

    public MyContext(SynchronizationContext context)
    {
        _context = context;
        _task = Task.CompletedTask;
        SetSynchronizationContext(this);
    }

    public override void Post(SendOrPostCallback d, object state)
    {
        if (_context != null)
            _context.Post(d, state);
        else
            Task.Run(() => 
            { 
                SetSynchronizationContext(this); 
                d(state); 
                SetSynchronizationContext(null); 
            });
    }

    public override void Send(SendOrPostCallback d, object state)
    {
        if (_context != null)
            _context.Send(d, state);
        else
            d(state);
    }

    public override void OperationStarted()
    {
        lock (this)
        {
            if (count++ == 0)
            {
                _src = new TaskCompletionSource();
                _task = _src.Task;
            }
        }
    }

    public override void OperationCompleted()
    {
        lock (this)
        {
            if (--count == 0)
                _src.SetResult();
        }
    }

    public ValueTask DisposeAsync()
    {
        SetSynchronizationContext(_context);
        return new ValueTask(_task);
    }
}

Далее вот так протестировал

class Program
{
    static async Task Main(string[] args)
    {
        Console.WriteLine("Start");
        await RunAsyncVoid(() => Method());
        Console.WriteLine("Done");
        Console.ReadKey(); 
    }

    private static async Task RunAsyncVoid(Action method)
    {
        await using var context = new MyContext(SynchronizationContext.Current);
        method();
    }

    static async void Method()
    {
        Console.WriteLine("Enter method 1");
        Method2();
        await Task.Delay(1000);
        Method3();
        Console.WriteLine("Finish method 1");
    }

    static async void Method2()
    {
        Console.WriteLine("Enter method 2");
        await Task.Delay(1000);
        Console.WriteLine("Finish method 2");
    }

    static async void Method3()
    {
        Console.WriteLine("Enter method 3");
        await Task.Delay(1000);
        Console.WriteLine("Finish method 3");
    }
}

И получил вот такой вывод в консоль.

Start
Enter method 1
Enter method 2
Finish method 2
Enter method 3
Finish method 1
Finish method 3
Done

Про плохую практику использования async void и все подводные камни я в курсе. Исключения перехватить оттуда невозможно - это факт. Но это всего-лишь тестовый пример, а речь на самом деле про обработчики событий и вызов события, среди которых могут быть и синхронные и асинхронные, вперемешку.

Вопрос академический: легально ли вот так подождать завершения async void метода, и какие грабли могут встретиться на пути? Просто я уже мысленно вижу как доработаю свой RelayCommand для WPF, но что-то боязно.


Ответы (0 шт):