当先行任务处于取消状态时执行 ContinueWith 以及在 ContinueWith 内部使用异步委托

Execution of ContinueWith when the antecedent Task is in canceled state and usage of async delegate inside of ContinueWith

此处讨论的代码是用 C# 编写并使用 .netcore 3.1 执行的

我有以下代码,它在后台启动工作负载而不等待它完成(即发即忘):

public void StartBackgroundWork(IAsyncDisposable resource, CancellationToken token)
{
    // some background work is started in a fire and forget manner
    _ = Task.Run(async () => 
    {
        
        try 
        {
            // here I perform my background work. Regardless of the outcome resource must be released as soon as possible
            // I want that cancellation requests coming from the provided cancellation token are correctly listened by this code
            // So, I pass the cancellation token everywhere
            
            await Task.Delay(1500, token);
        }
        finally 
        {
            // here I need to release the resource. Releasing this resource is important and must be done as soon as possible
            await resource.DisposeAsync();
        }       
    }, token);
}

重要的三点:

此代码的问题是取消标记被传递给 Task.Run 调用。如果令牌在 开始执行异步委托之前取消,则异步委托永远不会执行,因此永远不会执行 finally 块。这样做不满足释放 IAsyncDisposable 资源的要求(基本上,永远不会调用 DisposeAsync)。

解决此问题的最简单方法是 在调用 Task.Run 时提供取消标记。这样,异步委托总是被执行,因此 finally 块也被执行。 async delegate里面的代码监听了取消请求,所以也满足了取消执行的要求:

public void StartBackgroundWork(IAsyncDisposable resource, CancellationToken token)
{
    // some background work is started in a fire and forget manner
    _ = Task.Run(async () => 
    {
        
        try 
        {
            // here I perform my background work. Regardless of the outcome resource must be released as soon as possible
            // I want that cancellation requests coming from the provided cancellation token are correctly listened by this code
            // So, I pass the cancellation token everywhere
            
            await Task.Delay(1500, token);
        }
        finally 
        {
            // here I need to release the resource. Releasing this resource is important and must be done as soon as possible
            await resource.DisposeAsync();
        }       
    }, CancellationToken.None);
}

我在问自己,是否应该将 IAsyncDisposable 资源的释放委托给后续任务。使用这种方法重构的代码如下:

public void StartBackgroundWork(IAsyncDisposable resource, CancellationToken token)
{
    // some background work is started in a fire and forget manner
    _ = Task.Run(async () => 
    {
        // here I perform my background work. Regardless of the outcome resource must be released as soon as possible
        // I want that cancellation requests coming from the provided cancellation token are correctly listened by this code
        // So, I pass the cancellation token everywhere
        
        await Task.Delay(1500, token);
    }, 
    token).ContinueWith(async _ => 
    {
        // release the IAsyncDisposable resource here, afte the completion of the antecedent task and regardless
        // of the antecedent task actual state
        await resource.DisposeAsync();
    });
}

我不太熟悉 ContinueWith 陷阱,所以我的问题如下:

  1. 我是否可以保证继续 总是 执行,即使取消令牌在之前 执行前件被取消任务开始?
  2. ContinueWith 的调用提供异步委托有什么问题吗?异步委托的执行是否按预期完全完成?
  3. 最好的方法是什么?将 CancellationToken.None 传递给 Task.Run 的调用,或依赖于使用 ContinueWith 的延续 ?

重要说明:我知道使用 Task.Run 不是 服务器应用程序中的最佳方法(更多关于可以找到 here),所以可能有更好的方法来设计我的整体架构。我发布这个问题是为了更好地理解 ContinueWith 的实际行为,因为我不太熟悉它的用法(在现代 .NET 代码中,它在很大程度上被 async await 的用法所取代)。

您可以考虑使用 await using 语句,它自动处理 resource 的异步处理:

public async void StartBackgroundWork(IAsyncDisposable resource, CancellationToken token)
{
    await using var _ = resource;
    try
    {
        await Task.Run(async () => 
        {
            await Task.Delay(1500, token);
        }, token);
    } catch (OperationCanceledException) { }
}

我还将您的即发即弃任务转换为 async void(又名即发即崩溃)方法。万一发生不可想象的事情并且您的代码有错误,而不是应用程序继续 运行 发生了未观察到的异常,可能导致应用程序状态损坏,整个应用程序将崩溃,迫使您尽快修复错误。

但老实说,用一种方法创建一次性资源并用另一种方法处理它是一种糟糕的设计。理想情况下,创建资源的方法应该负责最终处理它。

我觉得;我只是要回答你的一些其他问题:

do I have the guarantee that the continuation is always executed, even if the cancellation token is canceled before the execution of the antecedent task starts ?

即使前面的任务已经完成,

ContinueWith 也会执行它的委托。在这种特定情况下,仅仅因为即发即弃的性质,就没有“保证”。

is there any issue in providing an async delegate to the invokation of ContinueWith ?

ContinueWith 不是 async 感知的,因此 ContinueWith 的 return 类型对大多数开发人员来说是令人惊讶的。由于您的代码丢弃了 return 类型,因此这不是问题。

Is the execution of the async delegate fully completed as expected ?

在这种情况下,很有可能,但这实际上取决于“预期”的含义。与所有其他即发即弃代码一样,您不能保证完成。 ContinueWith 还有一个问题:它使用 TaskScheduler 执行它的委托,默认的 TaskScheduler 不是 TaskScheduler.Default 但实际上是 TaskScheduler.Current。因此,如果您确实需要使用 ContinueWith.

,我总是建议传递明确的 TaskScheduler 以明确起见

What is the best approach ? Passing CancellationToken.None to the invokation of Task.Run, or relying on the continuation by using ContinueWith ?

只需将第二个参数丢给 Task.Run

我会更进一步:Task.Run 甚至不应该 一个 CancellationToken。我还没有看到它有用的场景。我怀疑 API 的 CancellationToken 部分是从 TaskFactory.StartNew 复制的( 很少有用的地方),但由于 Task.Run 总是使用 TaskScheduler.Default,提供 CancellationToken 在实践中没有用。

P.S。我最近在 proper solution for fire-and-forget on ASP.NET.

上写了一个简短的系列