如何无例外地取消任务?

时间:2019-05-10 09:02:08

标签: c# .net task task-parallel-library cancellation

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

由于我的任务运行时间长,并且在开始执行任务之前必须将其放入字典中,因此我必须使用new Task()。但是我遇到了不同的行为-在new Task()之后使用Cancel()创建任务时,它会抛出TaskCanceledException,而使用Task.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();
    }
}

UPD:

这种方法也无一例外地起作用

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)); ;
}

2 个答案:

答案 0 :(得分:0)

行为不一致的原因是,从根本上说,第一种情况下异步委托的使用不正确。 Task构造函数只是不接收Func<Task>,在与构造函数一起使用的情况下,您的异步委托始终被解释为async void而不是async Task。如果在async Task方法中引发了异常,则该异常将被捕获并放入Task对象中,而对于async void方法而言,这是不正确的,在这种情况下,异常会冒出气泡来同步上下文,它属于未处理异常的类别(您可以在this Stephen Cleary文章中了解详细信息)。因此,在使用构造函数的情况下会发生什么:将创建并启动一个应该启动异步流的任务。一旦到达Task.Delay(...)返回承诺的时间点,任务即完成,并且不再与Task.Delay延续中发生的任何事情相关(您可以通过将断点设置为{{1}来轻松地检入调试器} value.cts.Cancel()词典中的任务对象的状态为_tasks,而任务委托实际上仍在运行)。当请求取消时,在RanToCompletetion方法内引发了异常,并且不存在任何诺言对象被提升到应用程序域。

Task.Delay情况下,情况有所不同,因为此方法有很多重载,它们可以接受Task.RunFunc<Task>并在内部解包任务以返回底层承诺包装的任务,以确保Func<Task<T>>词典中正确的任务对象和正确的错误处理。

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

由于Unwrap()的使用存在某些危险,因此不认为它们是创建任务的野兽实践(请参见there)。但是,如果需要应用诸如Task.Factory.StartNew之类的特定选项,而不能直接与LongRunning配合使用,则可以在某些警告中使用它。

答案 1 :(得分:-2)

我最终得到了以下解决方案:

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

    var task = new Task(() =>
    {
        cts.Token.WaitHandle.WaitOne(waitBeforeExecute);
        if(cts.Token.IsCancellationRequested) return;
        action();
    }, cts.Token, TaskCreationOptions.LongRunning);


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

    task.Start(TaskScheduler.Default);
}