如何保证 UI 和非 UI 代码中 `ContinueWith` 中 `await` 的等效行为

问题描述 投票:0回答:1

我正在寻找最简单、最安全的方法来完全保证与

await
相同的行为,但使用
Task.ContinueWith
,最具体的是确保如果原始调用者位于 UI 线程中,则延续发生在 UI 线程上,否则它应该在任何线程上继续。我当前的方法在某些情况下会引发异常,我看不到明显的方法来提前预防。

我之前的理解是这段代码:

await DoSomethingAsync();
DoSomethingElse();

和这段代码:

DoSomethingAsync().ContinueWith(
     t => DoSomethingElse(),
     TaskScheduler.FromCurrentSynchronizationContext());

应该是等价的,如果原始调用线程是 UI 线程,都会导致

DoSomethingElse()
在原始调用线程上执行,否则延续线程将是未定义的。

但是,当没有定义 UI 线程或同步器时,

TaskScheduler.FromCurrentSynchronizationContext()
似乎至少在某些上下文中会失败。这种情况至少发生在 Mono WASM 项目中。 文档还提到,如果“当前 SynchronizationContext 不能用作 TaskScheduler”,该方法可能会失败,但它没有说明在尝试将其与
ContinueWith
一起使用之前如何检查是否是这种情况。

由于此代码将在从 WPF 到 Mono WASM 等许多不同的使用者之间共享,因此我需要一个适合所有这些使用者的解决方案,并且不需要使用者指定它是否在 UI 上下文中。

编写第二个块的最简单方法是什么,以便当实际上存在有效的同步上下文(例如在 WPF 应用程序中)时,在 UI 线程上发生延续 - 但如果没有,也不会引发异常一 - 完全像

await
那样吗?

c# wpf winforms async-await ui-thread
1个回答
0
投票

我不确定你为什么不使用

await
。使用
await
的代码比使用旧
ContinueWith
方法的代码更容易编写和维护。

也就是说,存在一些主要差异。首先是

await
捕获当前上下文;这是
SynchronizationContext.Current
TaskScheduler.Current
(或者根本没有上下文,这在逻辑上等同于
TaskScheduler.Default
)。
await
直接在该上下文上安排其延续,并且不会将
SynchronizationContext
包装到
TaskScheduler
中。

因此,如果您想使用

ContinueWith
,那么您可以使用
FromCurrentSynchronizationContext
,但已经存在语义差异:对于非
null
SynchronizationContext.Current
await
延续将具有
TaskScheduler.Current
等于
TaskScheduler.Default
,而
ContinueWith
延续将具有
TaskScheduler.Current
等于包装该
TaskScheduler
SynchronizationContext
实例。也许不是一个大问题,但请注意许多低级 TPL 方法隐式使用
TaskScheduler.Current
(包括
ContinueWith
)。

另一个主要区别是

ContinueWith
没有对异步代码的内置理解。因此,如果
DoSomethingElse
是异步的,那么行为将会有所不同。在这种情况下,
ContinueWith
将返回
Task<Task>
,您应该调用
Unwrap
来更紧密地模拟
await
的行为。

还有一些细微的差别;通过

TaskContinuationOptions.DenyChildAttach | TaskContinuationOptions.ExecuteSynchronously
应该更接近于
await
行为。您也可以尝试
TaskContinuationOptions.HideScheduler
,这应该会导致
TaskScheduler.Current
在延续中变为
TaskScheduler.Default

所以总而言之,这两个应该是大部分等价的:

await DoSomethingAsync();
DoSomethingElseSynchronously();

var continuationTask = DoSomethingAsync()
    .ContinueWith(
        _ => DoSomethingElseSynchronously(),
        CancellationToken.None,
        TaskContinuationOptions.DenyChildAttach | TaskContinuationOptions.ExecuteSynchronously | TaskContinuationOptions.HideScheduler,
        SynchronizationContext.Current == null ? TaskScheduler.Default : TaskScheduler.FromCurrentSynchronizationContext());

或者,如果异步:

await DoSomethingAsync();
await DoSomethingElseAsync();

var continuationTask = DoSomethingAsync()
    .ContinueWith(
        _ => DoSomethingElseAsync(),
        CancellationToken.None,
        TaskContinuationOptions.DenyChildAttach | TaskContinuationOptions.ExecuteSynchronously | TaskContinuationOptions.HideScheduler,
        SynchronizationContext.Current == null ? TaskScheduler.Default : TaskScheduler.FromCurrentSynchronizationContext())
    .Unwrap();
© www.soinside.com 2019 - 2024. All rights reserved.