C# Async/Await等效于.ContinueWith with CancellationToken和TaskScheduler.FromCurrentSynchronizationContext()调度器

C# Async/Await等效于.ContinueWith with CancellationToken和TaskScheduler.FromCurrentSynchronizationContext()调度器,c#,task-parallel-library,async-await,cancellation,C#,Task Parallel Library,Async Await,Cancellation,这是一项后续行动 问题:使用async/await而不是.ContinueWith来表达以下内容的简洁方式是什么 我主要感兴趣的是UI同步上下文的情况,例如Winforms 请注意,该行为具有以下所有所需的行为: 取消CancellationToken后,updateUITask将尽快取消,即长时间运行和可能抛出的工作可能仍将持续一段时间 在运行UpdateUI lambda之前,在UI线程上检查ct CancellationToken的取消情况,请参阅 在某些情况下,由于在执行UpdateUI

这是一项后续行动

问题:使用async/await而不是.ContinueWith来表达以下内容的简洁方式是什么

我主要感兴趣的是UI同步上下文的情况,例如Winforms

请注意,该行为具有以下所有所需的行为:

取消CancellationToken后,updateUITask将尽快取消,即长时间运行和可能抛出的工作可能仍将持续一段时间

在运行UpdateUI lambda之前,在UI线程上检查ct CancellationToken的取消情况,请参阅

在某些情况下,由于在执行UpdateUI lambda之前在UI线程上检查了ct CancellationToken,任务已完成或出现故障,因此updateUITask最终将被取消

在UI线程上检查CancellationToken和运行UpdateUI lambda之间没有中断流。也就是说,如果CancellationTokenSource仅在UI线程上被取消,那么在检查CancellationToken和运行UpdateUI lambda之间没有竞争条件,因为UI线程在这两个事件之间没有被放弃,所以在这两个事件之间没有任何东西可以触发CancellationToken

讨论:

我将此迁移到async/await的主要目标之一是从lambda中获得UpdateUI工作,以便于可读性/可调试性

1以上内容可通过以下方式解决。你可以在答案中随意使用

其他需求似乎很难封装到helper方法中,而不将UpdateUI作为lambda传递,因为我不能在检查CancellationToken和执行UpdateUI之间有一个中断,即等待,因为我假设我不能依赖wait同步使用executes的实现细节。这就是为什么Stephen提到的神秘的任务扩展方法.configureWaitCancellationToken会非常有用的地方

我现在已经发布了最好的答案,但我希望有人能想出更好的答案

演示用法的Winforms应用程序示例:

相关链接:


以下内容应等效:

var task = Task.Run(() => LongRunningAndMightThrow());

m_cts = new CancellationTokenSource();
CancellationToken ct = m_cts.Token;

try
{
    await task.WithCancellation(ct);
}
finally
{
    ct.ThrowIfCancellationRequested();
    UpdateUI(task);
}

请注意,如果LongRunningAndMightThrow方法出现故障,则需要try/finally,但当我们返回UI线程时,CancellationToken已被触发。如果没有它,返回的外部任务将出现故障,在原始ContinueWith情况下,它将被取消。

以下内容应是等效的:

var task = Task.Run(() => LongRunningAndMightThrow());

m_cts = new CancellationTokenSource();
CancellationToken ct = m_cts.Token;

try
{
    await task.WithCancellation(ct);
}
finally
{
    ct.ThrowIfCancellationRequested();
    UpdateUI(task);
}
请注意,如果LongRunningAndMightThrow方法出现故障,则需要try/finally,但当我们返回UI线程时,CancellationToken已被触发。如果没有它,返回的外部任务将出现故障,在原始ContinueWith情况下,它将被取消。

编写WithCancellation方法可以简单得多,只需一行代码:

public static Task WithCancellation(this Task task,
    CancellationToken token)
{
    return task.ContinueWith(t => t.GetAwaiter().GetResult(), token);
}
public static Task<T> WithCancellation<T>(this Task<T> task,
    CancellationToken token)
{
    return task.ContinueWith(t => t.GetAwaiter().GetResult(), token);
}
变化不大,但确实存在。您[可能]希望在启动新操作时取消上一个操作。如果该要求不存在,请删除相应的行。取消逻辑都已由WithCancellation处理,如果请求取消,则无需显式抛出,因为这已经发生了。实际上不需要将任务或取消令牌存储为局部变量。UpdateUI不应该接受任务,它应该只接受布尔值。在调用UpdateUI之前,应该从任务中解包该值。

编写WithCancellation方法可以简单得多,只需一行代码:

public static Task WithCancellation(this Task task,
    CancellationToken token)
{
    return task.ContinueWith(t => t.GetAwaiter().GetResult(), token);
}
public static Task<T> WithCancellation<T>(this Task<T> task,
    CancellationToken token)
{
    return task.ContinueWith(t => t.GetAwaiter().GetResult(), token);
}

变化不大,但确实存在。您[可能]希望在启动新操作时取消上一个操作。如果该要求不存在,请删除相应的行。取消逻辑都已由WithCancellation处理,如果请求取消,则无需显式抛出,因为这已经发生了。实际上不需要将任务或取消令牌存储为局部变量。UpdateUI不应该接受任务,它应该只接受布尔值。在调用UpdateUI之前,应先从任务中解包该值。

我喜欢带有取消功能的新oneliner。我想知道为什么Stephen没有使用它。您的解决方案与ContinueWith场景的行为不匹配:1取消令牌不一定在UI线程上检查,另外,在检查CancellationToken和运行UpdateUI之间,UI上可能会发生其他事情。2在某些情况下,外部任务将处于错误状态,其中ContinueWith案例将被取消。@MattSmith 1不需要在UI线程中检查取消标记;没有理由
让儿子这样做。2如果两个操作几乎同时发生,为什么会出现问题。这是一个竞赛条件,无论你如何编码,它将首先发生。这里没有真正的问题。3这是你自己的代码做同样的事情;它只在成功的情况下调用UpdateUI。这并不能改变这一点。如果您有代码要在出现错误时运行,请使用try/catch并将错误处理代码放入其中。@MattSmith您没有仔细考虑我描述的所有情况。可以单击“取消”按钮,并且在UI线程执行该单击事件处理程序之前,任务可能会出现故障。在用户单击按钮和相应操作实际发生之间存在延迟。在这段时间内,可能会发生一些事情,例如任务出错或正常完成。另一方面,你的任务可能会出错或正常完成,安排你必须继续做的事情,然后让用户在此期间单击“取消”。我喜欢新的带取消功能的oneliner。我想知道为什么Stephen没有使用它。您的解决方案与ContinueWith场景的行为不匹配:1取消令牌不一定在UI线程上检查,另外,在检查CancellationToken和运行UpdateUI之间,UI上可能会发生其他事情。2在某些情况下,外部任务将处于错误状态,其中ContinueWith案例将被取消。@MattSmith 1不需要在UI线程中检查取消标记;没有理由这样做。2如果两个操作几乎同时发生,为什么会出现问题。这是一个竞赛条件,无论你如何编码,它将首先发生。这里没有真正的问题。3这是你自己的代码做同样的事情;它只在成功的情况下调用UpdateUI。这并不能改变这一点。如果您有代码要在出现错误时运行,请使用try/catch并将错误处理代码放入其中。@MattSmith您没有仔细考虑我描述的所有情况。可以单击“取消”按钮,并且在UI线程执行该单击事件处理程序之前,任务可能会出现故障。在用户单击按钮和相应操作实际发生之间存在延迟。在这段时间内,可能会发生一些事情,例如任务出错或正常完成。另一方面,您的任务可能会出错或正常完成,请安排您必须继续执行的任务,然后让用户在此期间单击“取消”。
public static Task WithCancellation(this Task task,
    CancellationToken token)
{
    return task.ContinueWith(t => t.GetAwaiter().GetResult(), token);
}
public static Task<T> WithCancellation<T>(this Task<T> task,
    CancellationToken token)
{
    return task.ContinueWith(t => t.GetAwaiter().GetResult(), token);
}
m_cts.Cancel();
m_cts = new CancellationTokenSource();
var result = await Task.Run(() => LongRunningAndMightThrow())
    .WithCancellation(m_cts.Token);
UpdateUI(result);