ConfigureAwait(false)和库代码中的事件

4

我知道通常情况下在库代码中应该使用ConfigureAwait(false)。除非我知道续体需要调用方上下文。

但是,如果我不知道是否需要调用方上下文,我该怎么办呢? 我猜在这种情况下不应该使用ConfigureAwait(false),但我想确认一下。

例如:我的类有几个事件:

public static event TriggeredWebhookInfo WebhookTriggered;
public static event ReadonlyWebhookInfo WebhookCancelled;

这里有一个方法(它是private的,因为它被调用了一个返回public Task的方法,但它也使用了ConfigureAwait(false)):

private static async Task<HttpResponseMessage> TriggerTaskAsync(string url, WebhookBody body)
{
    if (body == null || url == null)
    {
        WebhookCancelled?.Invoke(url, body);
        Logger.Write(InternalTag.Sender, "Webhook cancelled: " + url, LogType.Normal);
        return null;
    }
    HttpResponseMessage response = await Client.PostAsync(url, GetContent(body)).ConfigureAwait(false);
    WebhookTriggered?.Invoke(url, body, response);
    Logger.Write(InternalTag.Sender, "Webhook triggered: " + url, LogType.Normal);
    return response;
}

当然,这对于任何终端应用程序完全可行。然而,如果事件在非终端应用程序中调用由用户分配的UI上下文上的方法会发生什么情况呢?Invoke()会减轻任何问题吗?还是我应该从调用此方法的调用堆栈中删除ConfigureAwait(false)


2
when I know that continuation will need caller context. that means, when your continuation needs to return to that context. Even if you know that the caller will want to return to its original context, let the caller decide. If in doubt, use ConfigureAwait(false) - Panagiotis Kanavos
调用者知道他们是否需要原始上下文。通常,您不应该假设哪个线程将调用事件并最终运行您的事件处理程序代码。 - Damien_The_Unbeliever
1
即使反转一切,它们也不应该是异步的 - 它们不能是。这就是为什么在事件处理程序中使用 async void 而不是 async Task。它们运行的线程应该被记录 - 是由控件/线程池/其他拥有的线程还是 UI 线程?您会发现事件文档说明事件是在此或那个事件上引发的。如果您希望它们在 UI 线程上运行,则可能需要采取额外的措施来查找 UI 同步上下文。除非 TriggerTaskAsycn 是从 UI 线程本身调用的,否则简单地删除 ConfigureAwait(false) 将无法做到这一点。 - Panagiotis Kanavos
在不同的线程上引发这些事件当然会让客户端程序员头痛不已。这是非常痛苦的,因为Canceled是在调用线程上引发的,但Triggered却不是。这里有一个非常简单的情况,就是不要自动使用它。无论是事件处理程序还是Logger.Write()都没有必要在另一个线程上运行。 - Hans Passant
2个回答

2
如果你想让方法处理程序与调用者拥有相同的上下文,你需要删除ConfigureAwait(false)。话虽如此,你也可以选择另一种方式;重要的是要记录下你的消费者应该期望什么。
另外,你可以使用IObservable<T>来发布事件,但这更加复杂,并引入了另一个(非平凡的)依赖项。

只有在从UI线程调用TriggerTaskAsync时,才能删除ConfigureAwait(false)。在响应后台处理的情况下在UI线程上引发事件(例如BackgroundWorker的方式)已经是一个复杂的情况,需要使用AsyncOperation、AsyncOperationManager。Observable可能更简单。 - Panagiotis Kanavos
@PanagiotisKanavos:哦,对了。我建议只记录上下文是否保留,而不是事件是否在UI线程上引发。根据OP的描述,这个库可能用于没有UI线程的情况。 - Stephen Cleary
这是正确的 - 实际上我会认为它更可能在终端应用程序中使用,并且它是针对这种用例开发的。但是,我可以想到一些用例是 UI 应用程序 - 我可能会自己编写一个。 - TehGM

2
你需要做出一个设计决策——事件是否保证在调用公共方法的同一上下文中返回?基于类似的用例,例如WebClientDownloadDataCompleted,当调用DownloadDataAsync时会触发此事件。一旦你决定了,请清楚地记录并确保遵守该规范。
一般来说,我会选择不在原始上下文中调用事件。如果调用者想要进行封送回传,它总是可以这样做的。但是,如果它不需要这样做,那么每次从异步调用返回时,你都会强制整个async链为了无意义的原始上下文而争夺资源。尽可能做到无上下文,并在最后一刻封送回传。
当然,你可以问自己是否需要同时使用事件和返回Task的方法来表示完全相同的异步事件。客户端是否正在监听WebHookTriggered事件并调用公共异步方法的相同代码?

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接