Як мені чекати подій у C #?


81

Я створюю клас, який має низку подій, одна з них 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 чимось асинхронним, що я можу використовувати? Я не впевнений , якщо я повинен використовувати BeginShutdownGameі EndShutdownGameз зворотними викликами, але це біль , тому що тільки тоді викликає джерело може передати функцію зворотного виклику, а не якась - або третя річ партії, штекери до двигуна, який є тим, що я отримую з подіями . Якщо сервер дзвонить game.ShutdownGame(), плагіни двигуна та / або інші компоненти движка не можуть передавати свої зворотні виклики, якщо я не підключаю якийсь спосіб реєстрації, зберігаючи колекцію зворотних викликів.

Будемо дуже вдячні за будь-яку пораду щодо того, яким найкращим / рекомендованим маршрутом рухатись цим шляхом! Я озирнувся навколо і здебільшого те, що я бачив, використовує підхід Begin / End, який, думаю, не задовольнить того, що я хочу зробити.

Редагувати

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

Моя думка з підходом зворотного виклику може спричинити витік пам'яті. Вимагання скасування реєстрації об’єктів покладається на те, що обробники очищають себе, а не ігровий движок, а просто дбають про це, як я хочу
Johnathon Sullinger

Чому ви обробляєте подію вимкнення з асинхронними обробниками подій? Це суть вашої проблеми, і вона здається нерозумною + непотрібною. Синхронність обробників забезпечує чистий, прямий механізм для того, щоб власник події знав, коли всі обробники завершені. Якщо поведінка асинхронного обробника є критичною, вам слід детальніше пояснити, чому це так. Якщо ви все-таки вирішили вибрати підхід "реєструвати зворотний дзвінок", не робіть це так, як ви показуєте; просто реалізуйте свою подію з Taskтипом повернення для делегата sig. а потім чекати у списку викликів (звичайно, після виклику)
Пітер Дуніхо

Я не підозрював, що ви можете зробити так, щоб делегати ваших подій мали тип повернення Завдання. Це могло б вирішити мою проблему разом, але коли я використовую Task як тип повернення, я отримую помилку компілятора, кажучи про неправильний тип повернення. Порожнеча виправляє це, але має побічний ефект, про який я згадав вище.
Джонатан Суллінгер

Де ви отримуєте помилку компілятора? Помилка означає, що ви неправильно реалізували мою пропозицію. Я розміщу відповідь із простим прикладом коду, що ілюструє, що я маю на увазі.
Пітер Дуніхо

Не могли б ви надати невеликий приклад згаданого вами підходу зворотного дзвінка? Я використовую асинхронне вимкнення, щоб я міг сповіщати об’єкти через движок (і сторонні плагіни) про те, що гра вимикається, і їм потрібно виконати очищення та збереження. Весь код збереження є асинхронним, саме тому я хочу, щоб вимкнення обробляло події асинхронними. В іншому випадку завершення завершується до того, як збереження буде закінчено, якщо це має сенс
Johnathon Sullinger

Відповіді:


92

Особисто я думаю, що наявність 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()) .


5
Для виклику всіх абонентів ви можете використовувати LINQ:await Task.WhenAll(handler.GetInvocationList().Select(invocation => ((Func<object, EventArgs, Task>)invocation)(this, EventArgs.Empty)));
LoRdPMN

4
FWIW, я зібрав AsyncEvent, який по суті робить те, що пропонує Пітер. Це запобіжний захід, поки Microsoft не впровадить належну підтримку.
Tagc

handlerTasks = Array.ConvertAll(invocationList, invocation => ((Func<object, EventArgs, Task>)invocation)(this, EventArgs.Empty)));
Бен Войгт,

6

Приклад Петра чудовий, я просто трохи спростив його за допомогою 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);
            }
        }
    }
}

Приємна відповідь. Методи розширення, безумовно, роблять його чистішим. Я отримав купу копій макаронних виробів, коли спочатку це робив.
Джонатан

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 => {зворотний виклик викликається після закінчення (синхронізуйте інтерфейс, коли це потрібно тут!)}, Нуль);

  3. підпишіться на подію клієнтом, як зазвичай


Як це працює? Ви не будете викликати передзвонення для кожного обробника?
johnny 5

див. приклад вище!
Martin.Martinsson

2
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace Example
{
    // delegate as alternative standard EventHandler
    public delegate Task AsyncEventHandler<TEventArgs>(object sender, TEventArgs e, CancellationToken token);


    public class ExampleObject
    {
        // use as regular event field
        public event AsyncEventHandler<EventArgs> AsyncEvent;

        // invoke using the extension method
        public async Task InvokeEventAsync(CancellationToken token) {
            await this.AsyncEvent.InvokeAsync(this, EventArgs.Empty, token);
        }

        // subscribe (add a listener) with regular syntax
        public static async Task UsageAsync() {
            var item = new ExampleObject();
            item.AsyncEvent += (sender, e, token) => Task.CompletedTask;
            await item.InvokeEventAsync(CancellationToken.None);
        }
    }


    public static class AsynEventHandlerExtensions
    {
        // invoke a async event (with null-checking)
        public static async Task InvokeAsync<TEventArgs>(this AsyncEventHandler<TEventArgs> handler, object sender, TEventArgs args, CancellationToken token) {
            var delegates = handler?.GetInvocationList();
            if (delegates?.Length > 0) {
                var tasks = delegates
                    .Cast<AsyncEventHandler<TEventArgs>>()
                    .Select(e => e.Invoke(sender, args, token));
                await Task.WhenAll(tasks);
            }
        }
    }
}

1

Це правда, події за своєю суттю непередбачувані, тому вам доведеться обійти їх.

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

Потім ви передаєте семафорний запис кожному з абонентів і дозволяєте їм робити свою справу до SemaphoreSlim.CurrentCount == amountOfSubscribers вони не звільнені.

Це по суті заблокує вашу програму, поки не закінчать усі абоненти подій.

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


Я б передав запис семафору через клас аргументації подій обробникам? Якщо так, чи буде Shutdownметод залежати від випуску семафору в обробниках, чи слід надавати зворотний виклик як аргумент події?
Джонатан Суллінгер

1
Хоча це буде працювати, кожен код обробника вимагає критичної частини коду для оновлення семафору, і вся передумова зазнає збою, якщо в кожному обробнику відсутній код оновлення. Якщо споживачеві все одно доведеться змінитись, я думаю, що нам слід вибрати рішення, яке не ґрунтується на подіях.
тіа

Розглядаючи підхід, заснований RegisterShutdownCallback(Func<Task> callback)на зворотному дзвінку, з яким слухачі звертаються, щоб зареєструвати очікуваний зворотний дзвінок. Потім, коли Shutdownвикликається, я переглядаю всі зареєстровані зворотні дзвінки. Це не так приємно, як проведення заходів, але це одне з можливих рішень
Johnathon Sullinger

1

Я знаю, що оператор конкретно запитував про використання async та завдань для цього, але ось альтернатива, яка означає, що обробникам не потрібно повертати значення. Код побудований на прикладі Пітера Дуніхо. Спочатку еквівалентний клас А (трохи зім'ятий, щоб підходити): -

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

Я сподіваюся, що це комусь корисно.


1

Якщо вам потрібно дочекатися стандартного обробника подій .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));
    }
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.