Как ждать событий в C#?

Я создаю класс, который имеет ряд мероприятий, одним из которых 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 и поэтому сделать его асинхронным-это в основном огонь и забыть. Мой движок активно использует eventing для уведомления сторонних разработчиков (и нескольких внутренних компонентов) о событиях, происходящих в движке, и позволяет им реагировать на них.

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

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

редактировать

другой вариант, который я рассматриваю, использует метод регистрации, который принимает ожидаемый обратный вызов. Я перебираю все обратные вызовы, хватаю их задачу и жду с 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);
}

4 ответов


лично я думаю, что имея 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");
    }
}

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


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

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

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

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

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


Я знаю, что 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); }
}

Я надеюсь, что это кому-то пригодится.


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);
                }
            });
        });
    }

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

  1. объявите свое событие в своем поставщике событий:

    публичное событие EventHandler DoSomething;

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

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

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