目录
介绍
C#中的Event早在使用async await进行异步编程之前就已经存在了。有时,了解如何将基于event的“旧” API调整为完全基于现代Task的API会很有用。
代码
让我们考虑一下这个简单的片段。我们有一个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;
}
}
}
这就是它的工作原理:
- 我们在Game类上创建一个返回Task的扩展方法。这样我们现在可以写await game.StartedAsync()
- 在该方法中,我们创建了一个实例TaskCompletionSource,我们将将其用作事件的“异步包装器”。
- 我们订阅了该Started事件。在处理程序中,我们只需将TaskCompletionSource的Task属性标记为已完成。
- 我们在Task上调用await。该方法将“异步等待”,直到Task完成,但在Game_Started处理程序中Task将标记为已完成。所以在这里,我们只是在等待StartedEvent被触发!
- 我们确保在完成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);
}
}
}
由于我们无法在事件上创建扩展方法,因此我们必须传递给“如何订阅”和“如何取消订阅”方法。
反射
我们可以从事件中使用反射到subscribe和unsubscribe。我们只需要传递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 (WPF、MAUI等的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