C#事件到可等待模式

目录

介绍

代码

我们可以推广这段代码吗?

订阅和取消订阅委托

反射

我们可以等待一个任务并确保等待后的代码同步运行吗?


介绍

C#中的Event早在使用async await进行异步编程之前就已经存在了。有时,了解如何将基于event” API调整为完全基于现代TaskAPI会很有用。

代码

让我们考虑一下这个简单的片段。我们有一个game类,有一个event告诉我们game什么时候开始。

var game = new Game();
game.Started += () =>
{
    Console.WriteLine("Game started");
};

class Game
{
    public event Action? Started;

    public void Start()
    {
        Started?.Invoke();
        Console.WriteLine("Game started notified");
    }
}

假设我们不能修Game改类,我们可以await事件吗

答案是肯定的:

var game = new Game();
await game.StartedAsync();

public static class GameExtensions
{
    public static async Task StartedAsync(this Game game)
    {
        var tcs = new TaskCompletionSource();
        game.Started += Game_Started;

        void Game_Started() => tcs.SetResult();
        try
        {
            await tcs.Task.ConfigureAwait(false);
        }
        finally
        {
            game.Started -= Game_Started;
        }
    }
}

这就是它的工作原理:

  1. 我们在Game类上创建一个返回Task的扩展方法。这样我们现在可以写await game.StartedAsync()
  2. 在该方法中,我们创建了一个实例TaskCompletionSource,我们将将其用作事件的“异步包装器”。
  3. 我们订阅了该Started事件。在处理程序中,我们只需将TaskCompletionSourceTask属性标记为已完成。 
  4. 我们在Task上调用await。该方法将“异步等待”,直到Task完成,但在Game_Started处理程序中Task将标记为已完成。所以在这里,我们只是在等待StartedEvent被触发!
  5. 我们确保在完成event处理程序后(在finally块中)取消订阅。

我们可以推广这段代码吗?

第一个想法是在event上创建一个扩展方法,以便能够编写如下内容:

await game.Started.WaitAsync();

不幸的是,C#不允许我们在事件上创建扩展方法。

我们有两种可能的解决方法:

订阅和取消订阅委托

var game = new Game();
await AsyncEx.WaitEvent(handler => game.Started += handler, 
                        handler => game.Started -= handler);

public static class AsyncEx
{
    public static async Task WaitEvent(Action<Action> subscriber, 
                                       Action<Action> unsubscriber)
    {
        var tcs = new TaskCompletionSource();

        subscriber.Invoke(SetResult);

        void SetResult() => tcs.SetResult();
        try
        {
            await tcs.Task.ConfigureAwait(false);
        }
        finally
        {
            unsubscriber.Invoke(SetResult);
        }
    }
}

由于我们无法在事件上创建扩展方法,因此我们必须传递给如何订阅如何取消订阅方法。

反射

我们可以从事件中使用反射到subscribeunsubscribe。我们只需要传递event的名称:

var game = new Game();
await game.WaitEvent(nameof(Game.Started));

public static class AsyncEx
{
    public static async Task WaitEvent<T>(this T @this, string eventName)
    {        
        var eventInfo = @this.GetType().GetEvent(eventName);
        var tcs = new TaskCompletionSource();
        Delegate handler = SetResult;
        eventInfo!.AddEventHandler(@this, handler);

        void SetResult() => tcs.SetResult();
        try
        {
            await tcs.Task.ConfigureAwait(false);
        }
        finally
        {
            eventInfo!.RemoveEventHandler(@this, handler);
        }
    }
}

重要

Event处理程序显然是同步的。这意味着每当有人出发Started event时,所有处理程序都会同步执行。

另一方面,在上面,我们正在等待Task,因此,在有人引Event件后,await之后的代码可能不会同步运行。就是这样,例如,如果我们正在等待Thread的自定义SynchronizationContext (WPFMAUI等的UI线程),但我们正在从另一个Thread完成Task。这是因为TaskAwaiter尝试在调用await之前恢复上下文,因此在自定义SynchronizationContext中推送了延续。

我们可以等待一个任务并确保等待后的代码同步运行吗?

嗯,不,但有点。我们必须放弃Task,并创建我们自己的自定义awaitable。这个想法是创建一个始终同步运行延续的awaitable

var game = new Game();
await game.WaitEvent(nameof(Game.Started));

public static class AsyncEx
{
    public static Awaitable WaitEvent<T>(this T @this, string eventName)
    {        
        var eventInfo = @this.GetType().GetEvent(eventName);
        var awaitable = new Awaitable();
        Delegate handler = null!;
        handler = SetResult;
        eventInfo!.AddEventHandler(@this, handler);
        void SetResult() => awaitable.ContinueWith(() => 
                            eventInfo!.RemoveEventHandler(@this, handler));
        return awaitable;
    }
}

public class Awaitable : INotifyCompletion
{
    Action? _continuation;
    Action? _continueWith;
    public Awaitable GetAwaiter() => this;
    public bool IsCompleted => false;
    public void OnCompleted(Action continuation)
    {
        _continuation = continuation;
        ExecuteContinuationIfPossible();
    }

    public void GetResult() { }
    public void ContinueWith(Action continueWith)
    {
        _continueWith = continueWith;
        ExecuteContinuationIfPossible();
    }

    void ExecuteContinuationIfPossible()
    {
        if (_continuation is null || _continueWith is null)
            return;

        _continuation.Invoke();
        _continueWith();
    }
}

https://www.codeproject.com/Tips/5359923/Csharp-Events-to-Awaitable-Pattern

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值