Подтвердить что ты не робот

Как мне ждать событий в С#?

Я создаю класс, который имеет ряд событий, один из которых GameShuttingDown. Когда это событие запущено, мне нужно вызвать обработчик события. Цель этого мероприятия - уведомить пользователей о завершении игры, и им необходимо сохранить свои данные. Сохранения ждут, а событий нет. Поэтому, когда обработчик получает вызов, игра завершается, прежде чем ожидаемые обработчики могут завершить.

public event EventHandler<EventArgs> GameShuttingDown;

public virtual async Task ShutdownGame()
{
    await this.NotifyGameShuttingDown();

    await this.SaveWorlds();

    this.NotifyGameShutDown();
}

private async Task SaveWorlds()
{
    foreach (DefaultWorld world in this.Worlds)
    {
        await this.worldService.SaveWorld(world);
    }
}

protected virtual void NotifyGameShuttingDown()
{
    var handler = this.GameShuttingDown;
    if (handler == null)
    {
        return;
    }

    handler(this, new EventArgs());
}

Регистрация события

// The game gets shut down before this completes because of the nature of how events work
DefaultGame.GameShuttingDown += async (sender, args) => await this.repo.Save(blah);

Я понимаю, что подпись для событий void EventName, и поэтому создание async - это в основном огонь и забыть. Мой движок активно использует eventing для уведомления сторонних разработчиков (и нескольких внутренних компонентов) о том, что события происходят внутри движка и позволяют им реагировать на них.

Есть ли хороший маршрут, чтобы спуститься, чтобы заменить событие чем-то асинхронным, на котором я могу использовать? Я не уверен, должен ли я использовать BeginShutdownGame и EndShutdownGame с обратными вызовами, но это боль, потому что тогда только вызывающий источник может передать обратный вызов, а не какой-либо сторонний материал, который подключается к движку, который что я получаю с событиями. Если сервер вызывает game.ShutdownGame(), то нет возможности подключать плагины и другие компоненты в двигателе по своим обратным вызовам, если я не подключу какой-либо метод регистрации, сохраняя коллекцию обратных вызовов.

Любые советы о том, какой предпочтительный/рекомендуемый маршрут будет сходить с этим, будут очень признательны! Я огляделся и по большей части увидел, что я использую подход Begin/End, который, как я думаю, не удовлетворит то, что я хочу делать.

Edit

Еще один вариант, который я рассматриваю, - это использовать метод регистрации, который требует ожидаемого обратного вызова. Я перебираю все обратные вызовы, захватываю их задачу и жду с помощью WhenAll.

private List<Func<Task>> ShutdownCallbacks = new List<Func<Task>>();

public void RegisterShutdownCallback(Func<Task> callback)
{
    this.ShutdownCallbacks.Add(callback);
}

public async Task Shutdown()
{
    var callbackTasks = new List<Task>();
    foreach(var callback in this.ShutdownCallbacks)
    {
        callbackTasks.Add(callback());
    }

    await Task.WhenAll(callbackTasks);
}
4b9b3361

Ответ 1

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

Тем не менее, если по какой-то причине вы или, по крайней мере, сильно вынуждены придерживаться этого дизайна, вы можете сделать это с помощью await -дружественного пути.

Ваша идея зарегистрировать обработчики и await их хороша. Тем не менее, я бы предложил придерживаться существующей парадигмы событий, так как это сохранит выразительность событий в вашем коде. Главное, что вам нужно отклоняться от стандартного типа делегата EventHandler и использовать тип делегата, который возвращает Task, чтобы вы могли await обработчики.

Вот простой пример, иллюстрирующий, что я имею в виду:

class A
{
    public event Func<object, EventArgs, Task> Shutdown;

    public async Task OnShutdown()
    {
        Func<object, EventArgs, Task> handler = Shutdown;

        if (handler == null)
        {
            return;
        }

        Delegate[] invocationList = handler.GetInvocationList();
        Task[] handlerTasks = new Task[invocationList.Length];

        for (int i = 0; i < invocationList.Length; i++)
        {
            handlerTasks[i] = ((Func<object, EventArgs, Task>)invocationList[i])(this, EventArgs.Empty);
        }

        await Task.WhenAll(handlerTasks);
    }
}

Метод OnShutdown() после выполнения стандарта "получить локальную копию экземпляра делегирования события" сначала вызывает все обработчики, а затем ожидает все возвращенные Tasks (сохраняя их в локальном массиве как обработчики вызываются).

Вот небольшая консольная программа, иллюстрирующая использование:

class Program
{
    static void Main(string[] args)
    {
        A a = new A();

        a.Shutdown += Handler1;
        a.Shutdown += Handler2;
        a.Shutdown += Handler3;

        a.OnShutdown().Wait();
    }

    static async Task Handler1(object sender, EventArgs e)
    {
        Console.WriteLine("Starting shutdown handler #1");
        await Task.Delay(1000);
        Console.WriteLine("Done with shutdown handler #1");
    }

    static async Task Handler2(object sender, EventArgs e)
    {
        Console.WriteLine("Starting shutdown handler #2");
        await Task.Delay(5000);
        Console.WriteLine("Done with shutdown handler #2");
    }

    static async Task Handler3(object sender, EventArgs e)
    {
        Console.WriteLine("Starting shutdown handler #3");
        await Task.Delay(2000);
        Console.WriteLine("Done with shutdown handler #3");
    }
}

Пройдя этот пример, я теперь задаюсь вопросом, не может ли быть С#, чтобы немного абстрагировать это. Возможно, это было бы слишком сложное изменение, но текущее сочетание старых обработчиков событий void и новой функции async/await кажется немного неудобным. Вышеприведенные работы (и хорошо работают, IMHO), но было бы неплохо иметь лучшую CLR и/или поддержку языка для сценария (т.е. Быть в состоянии ждать делегата многоадресной передачи и иметь компилятор С#, который превратит это в вызов WhenAll()).

Ответ 2

internal static class EventExtensions
{
    public static void InvokeAsync<TEventArgs>(this EventHandler<TEventArgs> @event, object sender,
        TEventArgs args, AsyncCallback ar, object userObject = null)
        where TEventArgs : class
    {
        var listeners = @event.GetInvocationList();
        foreach (var t in listeners)
        {
            var handler = (EventHandler<TEventArgs>) t;
            handler.BeginInvoke(sender, args, ar, userObject);
        }
    }
}

пример:

    public event EventHandler<CodeGenEventArgs> CodeGenClick;

        private void CodeGenClickAsync(CodeGenEventArgs args)
    {
        CodeGenClick.InvokeAsync(this, args, ar =>
        {
            InvokeUI(() =>
            {
                if (args.Code.IsNotNullOrEmpty())
                {
                    var oldValue = (string) gv.GetRowCellValue(gv.FocusedRowHandle, nameof(License.Code));
                    if (oldValue != args.Code)
                        gv.SetRowCellValue(gv.FocusedRowHandle, nameof(License.Code), args.Code);
                }
            });
        });
    }

Примечание. Это асинхронный режим, поэтому обработчик событий может поставить под угрозу поток пользовательского интерфейса. Обработчик событий (подписчик) не должен работать с пользовательским интерфейсом. В противном случае это не имело бы большого смысла.

  1. объявите свое мероприятие у провайдера:

    открытое событие EventHandler DoSomething;

  2. Вызвать событие вашего провайдера:

    DoSomething.InvokeAsync(new MyEventArgs(), this, ar => {обратный вызов вызывается после завершения (синхронизируйте пользовательский интерфейс, когда это необходимо!)}, Null);

  3. подписаться на событие клиентом, как вы это обычно делаете

Ответ 3

Питер пример великолепен, я только немного упростил его, используя LINQ и расширения:

public static class AsynchronousEventExtensions
{
    public static Task Raise<TSource, TEventArgs>(this Func<TSource, TEventArgs, Task> handlers, TSource source, TEventArgs args)
        where TEventArgs : EventArgs
    {
        if (handlers != null)
        {
            return Task.WhenAll(handlers.GetInvocationList()
                .OfType<Func<TSource, TEventArgs, Task>>()
                .Select(h => h(source, args)));
        }

        return Task.CompletedTask;
    }
}

Может быть хорошей идеей добавить время ожидания. Чтобы поднять событие, вызовите Поднять расширение:

public event Func<A, EventArgs, Task> Shutdown;

private async Task SomeMethod()
{
    ...

    await Shutdown.Raise(this, EventArgs.Empty);

    ...
}

Но вы должны знать, что, в отличие от синхронных событий, эта реализация вызывает обработчики одновременно. Это может быть проблемой, если обработчики должны выполняться строго последовательно, что они часто делают, например следующий обработчик зависит от результатов предыдущего:

someInstance.Shutdown += OnShutdown1;
someInstance.Shutdown += OnShutdown2;

...

private async Task OnShutdown1(SomeClass source, MyEventArgs args)
{
    if (!args.IsProcessed)
    {
        // An operation
        await Task.Delay(123);
        args.IsProcessed = true;
    }
}

private async Task OnShutdown2(SomeClass source, MyEventArgs args)
{
    // OnShutdown2 will start execution the moment OnShutdown1 hits await
    // and will proceed to the operation, which is not the desired behavior.
    // Or it can be just a concurrent DB query using the same connection
    // which can result in an exception thrown base on the provider
    // and connection string options
    if (!args.IsProcessed)
    {
        // An operation
        await Task.Delay(123);
        args.IsProcessed = true;
    }
}

Вам лучше изменить метод расширения для последовательного вызова обработчиков:

public static class AsynchronousEventExtensions
{
    public static async Task Raise<TSource, TEventArgs>(this Func<TSource, TEventArgs, Task> handlers, TSource source, TEventArgs args)
        where TEventArgs : EventArgs
    {
        if (handlers != null)
        {
            foreach (Func<TSource, TEventArgs, Task> handler in handlers.GetInvocationList())
            {
                await handler(source, args);
            }
        }
    }
}

Ответ 4

Это правда, события по своей сути не ожидаются, поэтому вам придется обойти это.

Одним из решений, которое я использовал в прошлом, является использование семафора, чтобы ждать, пока все записи в нем будут выпущены. В моей ситуации у меня было только одно подписанное событие, поэтому я мог бы жестко обозначить его как new SemaphoreSlim(0, 1), но в вашем случае вы можете переопределить getter/setter для своего события и сохранить счетчик количества подписчиков, чтобы вы могли динамически устанавливать максимальное количество одновременных потоков.

После этого вы передаете семафорную запись каждому подписчику и позволяете им делать свое дело до SemaphoreSlim.CurrentCount == amountOfSubscribers (иначе: все пятна были освобождены).

Это существенно блокирует вашу программу, пока все подписчики событий не закончили.

Возможно, вы также захотите рассмотреть вопрос о предоставлении для абонентов мероприятия à la GameShutDownFinished, которые они должны вызывать, когда они будут завершены с заданием на конец игры. В сочетании с перегрузкой SemaphoreSlim.Release(int) вы можете очистить все записи семафора и просто использовать Semaphore.Wait() для блокировки потока. Вместо того, чтобы проверять, были ли очищены все записи, вы теперь ждете, пока не будет освобождено одно место (но должен быть только один момент, когда все пятна освобождаются сразу).

Ответ 5

Я знаю, что op задавал вопрос об использовании async и задач для этого, но вот альтернатива, которая означает, что обработчикам не нужно возвращать значение. Код основан на примере Питера Дунихо. Сначала эквивалентный класс A (немного сжатый): -

class A
{
    public delegate void ShutdownEventHandler(EventArgs e);
    public event ShutdownEventHandler ShutdownEvent;
    public void OnShutdownEvent(EventArgs e)
    {
        ShutdownEventHandler handler = ShutdownEvent;
        if (handler == null) { return; }
        Delegate[] invocationList = handler.GetInvocationList();
        Parallel.ForEach<Delegate>(invocationList, 
            (hndler) => { ((ShutdownEventHandler)hndler)(e); });
    }
}

Простое консольное приложение, чтобы показать его использование...

using System;
using System.Threading;
using System.Threading.Tasks;

...

class Program
{
    static void Main(string[] args)
    {
        A a = new A();
        a.ShutdownEvent += Handler1;
        a.ShutdownEvent += Handler2;
        a.ShutdownEvent += Handler3;
        a.OnShutdownEvent(new EventArgs());
        Console.WriteLine("Handlers should all be done now.");
        Console.ReadKey();
    }
    static void handlerCore( int id, int offset, int num )
    {
        Console.WriteLine("Starting shutdown handler #{0}", id);
        int step = 200;
        Thread.Sleep(offset);
        for( int i = 0; i < num; i += step)
        {
            Thread.Sleep(step);
            Console.WriteLine("...Handler #{0} working - {1}/{2}", id, i, num);
        }
        Console.WriteLine("Done with shutdown handler #{0}", id);
    }
    static void Handler1(EventArgs e) { handlerCore(1, 7, 5000); }
    static void Handler2(EventArgs e) { handlerCore(2, 5, 3000); }
    static void Handler3(EventArgs e) { handlerCore(3, 3, 1000); }
}

Я надеюсь, что это кому-то полезно.

Ответ 6

Если вам нужно дождаться стандартного обработчика событий .net, вы не сможете этого сделать, потому что он void.

Но вы можете создать асинхронную систему событий для этого:

public delegate Task AsyncEventHandler(AsyncEventArgs e);

public class AsyncEventArgs : System.EventArgs
{
    public bool Handled { get; set; }
}

public class AsyncEvent
{
    private string name;
    private List<AsyncEventHandler> handlers;
    private Action<string, Exception> errorHandler;

    public AsyncEvent(string name, Action<string, Exception> errorHandler)
    {
        this.name = name;
        this.handlers = new List<AsyncEventHandler>();
        this.errorHandler = errorHandler;
    }

    public void Register(AsyncEventHandler handler)
    {
        if (handler == null)
            throw new ArgumentNullException(nameof(handler));

        lock (this.handlers)
            this.handlers.Add(handler);
    }

    public void Unregister(AsyncEventHandler handler)
    {
        if (handler == null)
            throw new ArgumentNullException(nameof(handler));

        lock (this.handlers)
            this.handlers.Remove(handler);
    }

    public IReadOnlyList<AsyncEventHandler> Handlers
    {
        get
        {
            var temp = default(AsyncEventHandler[]);

            lock (this.handlers)
                temp = this.handlers.ToArray();

            return temp.ToList().AsReadOnly();
        }
    }

    public async Task InvokeAsync()
    {
        var ev = new AsyncEventArgs();
        var exceptions = new List<Exception>();

        foreach (var handler in this.Handlers)
        {
            try
            {
                await handler(ev).ConfigureAwait(false);

                if (ev.Handled)
                    break;
            }
            catch(Exception ex)
            {
                exceptions.Add(ex);
            }
        }

        if (exceptions.Any())
            this.errorHandler?.Invoke(this.name, new AggregateException(exceptions));
    }
}

И теперь вы можете объявить свои асинхронные события:

public class MyGame
{
    private AsyncEvent _gameShuttingDown;

    public event AsyncEventHandler GameShuttingDown
    {
        add => this._gameShuttingDown.Register(value);
        remove => this._gameShuttingDown.Unregister(value);
    }

    void ErrorHandler(string name, Exception ex)
    {
         // handle event error.
    }

    public MyGame()
    {
        this._gameShuttingDown = new AsyncEvent("GAME_SHUTTING_DOWN", this.ErrorHandler);.
    }
}

И вызовите ваше асинхронное событие, используя:

internal async Task NotifyGameShuttingDownAsync()
{
    await this._gameShuttingDown.InvokeAsync().ConfigureAwait(false);
}

Общая версия:

public delegate Task AsyncEventHandler<in T>(T e) where T : AsyncEventArgs;

public class AsyncEvent<T> where T : AsyncEventArgs
{
    private string name;
    private List<AsyncEventHandler<T>> handlers;
    private Action<string, Exception> errorHandler;

    public AsyncEvent(string name, Action<string, Exception> errorHandler)
    {
        this.name = name;
        this.handlers = new List<AsyncEventHandler<T>>();
        this.errorHandler = errorHandler;
    }

    public void Register(AsyncEventHandler<T> handler)
    {
        if (handler == null)
            throw new ArgumentNullException(nameof(handler));

        lock (this.handlers)
            this.handlers.Add(handler);
    }

    public void Unregister(AsyncEventHandler<T> handler)
    {
        if (handler == null)
            throw new ArgumentNullException(nameof(handler));

        lock (this.handlers)
            this.handlers.Remove(handler);
    }

    public IReadOnlyList<AsyncEventHandler<T>> Handlers
    {
        get
        {
            var temp = default(AsyncEventHandler<T>[]);

            lock (this.handlers)
                temp = this.handlers.ToArray();

            return temp.ToList().AsReadOnly();
        }
    }

    public async Task InvokeAsync(T ev)
    {
        var exceptions = new List<Exception>();

        foreach (var handler in this.Handlers)
        {
            try
            {
                await handler(ev).ConfigureAwait(false);

                if (ev.Handled)
                    break;
            }
            catch (Exception ex)
            {
                exceptions.Add(ex);
            }
        }

        if (exceptions.Any())
            this.errorHandler?.Invoke(this.name, new AggregateException(exceptions));
    }
}