从同步到异步场景

From sync to async scenario

我有这个事件处理程序:

private void OnShutdownRequested(object? sender, ShutdownRequestedEventArgs e)
{
   var canShutdown = lifetime.CanShutdown();
   e.Cancel = !canShutdown;
}

现在,由于设计决定,CanShutdown 方法已从 bool 更改为 Task<bool>

Task<bool> CanShutDown()
{
   //...
}

所以,我需要像这样修改事件处理程序:

private async void OnShutdownRequested(object? sender, ShutdownRequestedEventArgs e)
{
   var canShutdown = await lifetime.CanShutdown();
   e.Cancel = !canShutdown;
}

我读过很多次 async void 方法有很多问题,比如直接在 SynchronizationContext 中抛出未处理的异常。但是它们的一种有效用法是事件处理程序。这是一个事件处理程序。不是吗?

但是,我想知道在“迁移”之后该代码是否没有不良后果

我担心的是处理程序修改了e.Cancel的值。

一位同事告诉我这会发生:

After await, the caller to that method isn't awaiting. It assumes synchronous execution, so it immediately reads e.Cancel, and that hasn't been set yet. That is a problem inside event handler: You realize as soon as the await keyword is hit, the code that called ShutdownRequested.Invoke() immediately returns. The value it will read might not be up-to-date.

恐怕我的同事有他的观点。所以,这种方法似乎被打破了。但我仍然不知道如何解决这个问题。

如何处理EventArgs被同步和异步代码共享?

您的同事是正确的,发布的示例代码很可能无法按预期工作。

我建议让 ShutdownRequestedEventArgs 包含类似 List<Task<bool>> 的内容,这样调用者可以等待所有任务以确定是否有任何任务要在关闭前取消。这可能还包括超时,以便某些挂起的任务不会永远阻塞进程。这将问题从事件的处理程序转移到调用方,希望调用方能够更好地处理问题。

另一种可能是同步等待任务:

private void OnShutdownRequested(object? sender, ShutdownRequestedEventArgs e)
{
   var canShutdown = lifetime.CanShutdown().Result;
   e.Cancel = !canShutdown;
}

但这在 UI 程序中可能很危险。如果在UI线程上请求ShutdownRequested,而CanShutdown需要在UI线程上执行任何代码,那么程序就会死锁。如果 CanShutdown 不是专门为避免这种情况而编写的,则这种情况很有可能发生。有关详细信息,请参阅 configureAwait

您也可以恢复为同步解决方案。像 ShutdownRequested 这样的事件听起来不太适合异步解决方案。我原以为这样的事件需要立即响应。但我不知道你申请的背景,也不知道当初为什么要改方法,所以很可能是有充分理由的。

I've read many times that async void methods have many problems, like unhandled exceptions being thrown directly inside the SynchronizationContext. But one of the valid usages for them are event handlers.

是的。事实上,在 SynchronizationContext 上抛出异常实际上是故意的行为,专门用于模拟事件处理程序的行为。

async void 方法的主要问题之一是很难确定 async void 方法何时完成。这正是你的同事指出的问题。

This is an event handler. Isn't it?

有点。

退后一步,考虑一下所使用的设计模式。 Observer pattern 是一种通知观察者状态变化的方法。观察者模式非常适合 OOP 中的“事件”:任意数量的观察者都可以订阅状态更改通知。

不过,这种“关机”通知不仅仅是 通知。它还有一个 return 值。一般来说,这是Strategy pattern。策略模式 非常适合 OOP 中的事件。然而,很多时候策略模式是通过事件(错误)实现的;这是 OOP 语言中常见的设计错误。

那么,它是事件处理程序吗?从技术上讲,是的。 应该它是一个事件处理程序吗?可能不是。

在同步世界中,使用事件实现策略模式通常“足够接近”。有时对于在有多个事件订阅者的情况下应该如何处理 return 值存在一些困惑,但通常这种设计错误不会被注意到。直到async的出现,突然间,Strategy模式使用事件的设计错误变得更加明显。

But I still don't see how to fix that.

我描述几个possibilities on my blog.

如果您控制 OnShutdownRequestedShutdownRequestedEventArgs,那么您可以使用延期。设置起来有点复杂,但它允许同步和异步处理程序(只要处理程序使用延迟),并且引发事件的代码可以(异步地)等待所有处理程序完成后再检索结果。

如果关机是您唯一需要担心的事件,那么一个常见的技巧是始终将 Cancel 设置为 true,执行异步工作,然后如果允许关机,在该异步工作结束时明确关闭。