program tip

C #에서 이벤트를 어떻게 기다리나요?

radiobox 2020. 11. 25. 07:50
반응형

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있으므로 비동기로 만드는 것이 기본적으로 발생하고 잊어 버린다는 것을 이해합니다 . 내 엔진은 이벤트를 많이 사용하여 타사 개발자 (및 여러 내부 구성 요소)에 이벤트가 엔진 내에서 발생하고 있음을 알리고 이에 반응하도록합니다.

이벤트를 내가 사용할 수있는 비동기 기반으로 대체 할 수있는 좋은 경로가 있습니까? 내가 사용해야하는지 잘 모르겠어요 BeginShutdownGameEndShutdownGame콜백으로, 그러나 오직 호출 소스가 콜백을 통과 할 수 있기 때문에 그는 고통되지 않은 제 3 자 물건이 내가 이벤트와 얻고 무엇을 엔진에의 플러그 . 서버가를 호출 game.ShutdownGame()하면 콜백 모음을 유지하면서 일종의 등록 방법을 연결하지 않는 한 엔진 플러그인 및 엔진 내의 다른 구성 요소가 콜백을 전달할 방법이 없습니다.

이것으로 내려가는 선호 / 권장 경로에 대한 조언은 크게 감사하겠습니다! 나는 주위를 둘러 보았고 내가 본 것은 대부분 내가하고 싶은 것을 만족시킬 것이라고 생각하지 않는 시작 / 끝 접근 방식을 사용하는 것입니다.

편집하다

내가 고려중인 또 다른 옵션은 대기 가능한 콜백을받는 등록 방법을 사용하는 것입니다. 나는 모든 콜백을 반복하고 그들의 Task를 잡고 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);
}

개인적으로 저는 async이벤트 핸들러 를 갖는 것이 최선의 디자인 선택이 아닐 수 있다고 생각합니다 . 동기식 핸들러를 사용하면 완료시기를 쉽게 알 수 있습니다.

즉, 어떤 이유로 든이 디자인을 고수해야하거나 적어도 강요한다면 await친근한 방식으로 할 수 있습니다 .

핸들러와 await그것들 을 등록하는 당신의 아이디어 는 좋은 것입니다. 그러나 기존 이벤트 패러다임을 고수하는 것이 좋습니다. 그러면 코드에서 이벤트의 표현력이 유지됩니다. 가장 중요한 것은 표준 EventHandler기반 대리자 형식 에서 벗어나 처리기 Task를 사용할 수 있도록 a를 반환하는 대리자 형식을 사용해야 한다는 것 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기능 의 현재 혼합은 약간 어색해 보입니다. 위의 작품 (그리고 잘 이럴 작동)하지만,이 시나리오에 대한 더 나은 CLR 및 / 또는 언어 지원을하는 것이었다 좋은있을 것입니다 (즉, 멀티 캐스트 대리자를 기다리고와 C # 컴파일러 회전을 가질 수 그에게 전화로 WhenAll()) .


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 스레드를 손상시킬 수 있습니다. 이벤트 핸들러 (구독자)는 UI 작업을 수행하지 않아야합니다. 그렇지 않으면 의미가 없습니다.

  1. 이벤트 제공자에서 이벤트를 선언하십시오.

    공개 이벤트 EventHandler DoSomething;

  2. 제공자의 이벤트를 호출하십시오.

    DoSomething.InvokeAsync (new MyEventArgs (), this, ar => {완료시 호출되는 콜백 (여기에 필요한 경우 UI 동기화!)}, null);

  3. 평소처럼 클라이언트별로 이벤트를 구독하십시오.


Peter의 예는 훌륭합니다. 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;
    }
}

시간 제한을 추가하는 것이 좋습니다. 이벤트 호출 Raise 확장을 발생 시키려면 :

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

사실, 이벤트는 본질적으로 기다릴 수 없으므로 문제를 해결해야합니다.

One solution I have used in the past is using a semaphore to wait for all entries in it to be released. In my situation I only had one subscribed event so I could hardcode it as new SemaphoreSlim(0, 1) but in your case you might want to override the getter/setter for your event and keep a counter of how many subscribers there are so you can dynamically set the max amount of simultaneous threads.

Afterwards you pass a semaphore entry to each of the subscribers and let them do their thing until SemaphoreSlim.CurrentCount == amountOfSubscribers (aka: all spots have been freed).

This would essentially block your program until all event subscribers have finished.

You might also want to consider providing an event à la GameShutDownFinished for your subscribers, which they have to call when they're done with their end-of-game task. Combined with the SemaphoreSlim.Release(int) overload you can now clear up all semaphore entries and simply use Semaphore.Wait() to block the thread. Instead of having to check whether or not all entries have been cleared you now wait until one spot has been freed (but there should only one moment where all spots are freed at once).


I know that the op was asking specifically about using async and tasks for this, but here is an alternative that means the handlers do not need to return a value. The code is based on Peter Duniho's example. First the equivalent class A (squashed up a bit to fit) :-

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

A simple console application to show its use...

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

I hope that this is useful to someone.

참고URL : https://stackoverflow.com/questions/27761852/how-do-i-await-events-in-c

반응형