首页 > 解决方案 > 如何毫无例外地取消任务?

问题描述

LongRunning我需要延迟后执行一种任务。每个任务都可以取消。我更TPL喜欢cancellationToken.

由于我的任务运行时间很长,并且在开始任务之前必须将其放入字典中,因此我必须使用new Task(). 但是我遇到了不同的行为——当任务是在它抛出new Task()之后创建的,而使用它创建的任务不会抛出异常。Cancel()TaskCanceledExceptionTask.Run

通常我需要认识到差异而不是得到TaskCanceledException.

这是我的代码:

internal sealed class Worker : IDisposable
{
    private readonly IDictionary<Guid, (Task task, CancellationTokenSource cts)> _tasks =
        new Dictionary<Guid, (Task task, CancellationTokenSource cts)>();

    public void ExecuteAfter(Action action, TimeSpan waitBeforeExecute, out Guid cancellationId)
    {
        var cts = new CancellationTokenSource();

        var task = new Task(async () =>
        {
            await Task.Delay(waitBeforeExecute, cts.Token);
            action();
        }, cts.Token, TaskCreationOptions.LongRunning);

        cancellationId = Guid.NewGuid();
        _tasks.Add(cancellationId, (task, cts));

        task.Start(TaskScheduler.Default);
    }

    public void ExecuteAfter2(Action action, TimeSpan waitBeforeExecute, out Guid cancellationId)
    {
        var cts = new CancellationTokenSource();
        cancellationId = Guid.NewGuid();
        _tasks.Add(cancellationId, (Task.Run(async () =>
        {
            await Task.Delay(waitBeforeExecute, cts.Token);
            action();
        }, cts.Token), cts));
    }

    public void Abort(Guid cancellationId)
    {
        if (_tasks.TryGetValue(cancellationId, out var value))
        {
            value.cts.Cancel();
            //value.task.Wait();

            _tasks.Remove(cancellationId);
            Dispose(value.cts);
            Dispose(value.task);
        }
    }

    public void Dispose()
    {
        if (_tasks.Count > 0)
        {
            foreach (var t in _tasks)
            {
                Dispose(t.Value.cts);
                Dispose(t.Value.task);
            }

            _tasks.Clear();
        }
    }

    private static void Dispose(IDisposable obj)
    {
        if (obj == null)
        {
            return;
        }

        try
        {
            obj.Dispose();
        }
        catch (Exception ex)
        {
            //Log.Exception(ex);
        }
    }
}

internal class Program
{
    private static void Main(string[] args)
    {
        Action act = () => Console.WriteLine("......");

        Console.WriteLine("Started");
        using (var w = new Worker())
        {
            w.ExecuteAfter(act, TimeSpan.FromMilliseconds(10000), out var id);
            //w.ExecuteAfter2(act, TimeSpan.FromMilliseconds(10000), out var id);
            Thread.Sleep(3000);
            w.Abort(id);
        }

        Console.WriteLine("Enter to exit");
        Console.ReadKey();
    }
}

升级版:

这种方法也无一例外地有效

public void ExecuteAfter3(Action action, TimeSpan waitBeforeExecute, out Guid cancellationId)
{
    var cts = new CancellationTokenSource();
    cancellationId = Guid.NewGuid();

    _tasks.Add(cancellationId, (Task.Factory.StartNew(async () =>
    {
        await Task.Delay(waitBeforeExecute, cts.Token);
        action();
    }, cts.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default), cts)); ;
}

标签: c#.nettasktask-parallel-librarycancellation

解决方案


不一致行为的原因是从根本上不正确地使用了第一种情况下的异步委托。构造Task函数只是不接收Func<Task>,并且您的异步委托总是被解释为async voidasync Task使用构造函数的情况。如果在方法中引发异常,async Task它会被捕获并放入Task对象中,这对于方法来说是不正确的async void,在这种情况下,异常只会从方法中冒泡到同步上下文中,并属于未处理异常的类别(您可以熟悉这篇Stephen Cleary 文章中的详细信息)。那么在使用构造函数的情况下会发生什么:创建并启动一个应该启动异步流程的任务。一旦到达点Task.Delay(...)返回一个承诺,任务完成并且它与Task.Delay继续发生的任何事情都没有关系(您可以通过设置断点来轻松检查调试器,以确保字典value.cts.Cancel()中的任务对象具有状态,而任务委托基本上仍在运行) . 当请求取消时,方法内部会引发异常,并且不存在任何 Promise 对象被提升到应用程序域。_tasksRanToCompletetionTask.Delay

在情况Task.Run不同的情况下,因为此方法的重载能够在内部接受Func<Task>Func<Task<T>>解包任务,以返回底层承诺而不是包装任务,从而确保_tasks字典中的正确任务对象和正确的错误处理。

第三种情况尽管它没有抛出异常,但它是部分正确的。不像Task.Run,Task.Factory.StartNew不会打开底层任务以返回承诺,因此存储在 中的_tasks任务只是包装器任务,就像在构造函数的情况下一样(您可以再次使用调试器检查其状态)。但是它能够理解Func<Task>参数,因此异步委托具有async Task签名,至少允许在底层任务中处理和存储异常。为了获得这个基础任务,Task.Factory.StartNew您需要使用Unwrap()扩展方法自己解开任务。

由于Task.Factory.StartNew与它的应用程序相关的某些危险,它不被视为创建任务的野兽实践(参见那里)。但是,如果您需要应用LongRunning无法直接应用的特定选项,则它可以与一些警告一起使用Task.Run


推荐阅读