【问题标题】:How to cancel Task without exception?如何毫无例外地取消任务?
【发布时间】:2019-05-10 09:02:08
【问题描述】:

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

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

【问题讨论】:

  • 您可以对照枚举docs.microsoft.com/de-de/dotnet/api/…检查任务的Status属性
  • 它对我有什么帮助? Task.Run 不会抛出异常,而 new Task() 会。我需要弄清楚为什么会这样。
  • 他们都抛出异常老兄
  • @JohnChris 这是因为//value.task.Wait();我刚刚更新了我的代码
  • @isxaker 不要使用new Task。任务不是线程,它们是承诺。没有理由使用冷任务。 Task.Run 了解取消,returns a task in the cancelled state 了解令牌是否已取消。

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


【解决方案1】:

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

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

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

Task.Factory.StartNew 不被视为创建任务的野兽实践,因为它的应用程序存在某些危险(请参阅there)。但是,如果您需要应用无法直接与Task.Run 一起应用的特定选项,例如LongRunning,则可以使用它并注意一些事项。

【讨论】:

    【解决方案2】:

    我不知道为什么我在这里投了反对票,但这激发了我更新我的答案。

    更新

    我的完整方法:

    using System;
    using System.Collections.Generic;
    using System.Threading;
    using System.Threading.Tasks;
    
    namespace ConsoleApp4
    {
        internal class Program
        {
            private static void Main(string[] args)
            {
                using (var delayedWorker = new DelayedWorker())
                {
                    delayedWorker.ProcessWithDelay(() => { Console.WriteLine("100"); }, TimeSpan.FromSeconds(5), out var cancellationId_1);
                    delayedWorker.ProcessWithDelay(() => { Console.WriteLine("200"); }, TimeSpan.FromSeconds(10), out var cancellationId_2);
                    delayedWorker.ProcessWithDelay(() => { Console.WriteLine("300"); }, TimeSpan.FromSeconds(15), out var cancellationId_3);
    
                    Cancel_3(delayedWorker, cancellationId_3);
    
                    Console.ReadKey();
                }
            }
    
            private static void Cancel_3(DelayedWorker delayedWorker, Guid cancellationId_3)
            {
                Task.Run(() => { delayedWorker.Abort(cancellationId_3); }).Wait();
            }
    
            internal sealed class DelayedWorker : IDisposable
            {
                private readonly object _locker = new object();
                private readonly object _disposeLocker = new object();
                private readonly IDictionary<Guid, (Task task, CancellationTokenSource cts)> _tasks = new Dictionary<Guid, (Task task, CancellationTokenSource cts)>();
                private bool _disposing;
    
                public void ProcessWithDelay(Action action, TimeSpan waitBeforeExecute, out Guid cancellationId)
                {
                    Console.WriteLine("Creating delayed action...");
                    CancellationTokenSource tempCts = null;
                    CancellationTokenSource cts = null;
    
                    try
                    {
                        var id = cancellationId = Guid.NewGuid();
    
                        tempCts = new CancellationTokenSource();
                        cts = tempCts;
    
                        var task = new Task(() => { Process(action, waitBeforeExecute, cts); }, TaskCreationOptions.LongRunning);
                        _tasks.Add(cancellationId, (task, cts));
                        tempCts = null;
    
                        task.ContinueWith(t =>
                        {
                            lock (_disposeLocker)
                            {
                                if (!_disposing)
                                {
                                    TryRemove(id);
                                }
                            }
                        }, TaskContinuationOptions.ExecuteSynchronously);
    
                        Console.WriteLine($"Created(cancellationId: {cancellationId})");
                        task.Start(TaskScheduler.Default);
                    }
                    finally
                    {
                        if (tempCts != null)
                        {
                            tempCts.Dispose();
                        }
                    }
                }
    
                private void Process(Action action, TimeSpan waitBeforeExecute, CancellationTokenSource cts)
                {
                    Console.WriteLine("Starting delayed action...");
                    cts.Token.WaitHandle.WaitOne(waitBeforeExecute);
                    if (cts.Token.IsCancellationRequested)
                    {
                        return;
                    }
    
                    lock (_locker)
                    {
                        Console.WriteLine("Performing action...");
                        action();
                    }
                }
    
                public bool Abort(Guid cancellationId)
                {
                    Console.WriteLine($"Aborting(cancellationId: {cancellationId})...");
    
                    lock (_locker)
                    {
                        if (_tasks.TryGetValue(cancellationId, out var value))
                        {
                            if (value.task.IsCompleted)
                            {
                                Console.WriteLine("too late");
                                return false;
                            }
    
                            value.cts.Cancel();
                            value.task.Wait();
    
                            Console.WriteLine("Aborted");
                            return true;
                        }
    
                        Console.WriteLine("Either too late or wrong cancellation id");
                        return true;
                    }
                }
    
                private void TryRemove(Guid id)
                {
                    if (_tasks.TryGetValue(id, out var value))
                    {
                        Remove(id, value.task, value.cts);
                    }
                }
    
                private void Remove(Guid id, Task task, CancellationTokenSource cts)
                {
                    _tasks.Remove(id);
    
                    Dispose(cts);
                    Dispose(task);
                }
    
                public void Dispose()
                {
                    lock (_disposeLocker)
                    {
                        _disposing = true;
                    }
    
                    if (_tasks.Count > 0)
                    {
                        foreach (var t in _tasks)
                        {
                            t.Value.cts.Cancel();
                            t.Value.task.Wait();
    
                            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 ex
                    }
                }
            }
        }
    }
    

    【讨论】:

    • WaitOne 内部任务不好,因为它是一个阻塞操作,可能会导致以后很难调试。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-11-29
    • 1970-01-01
    • 2014-12-26
    • 2018-12-21
    • 1970-01-01
    • 2016-12-27
    相关资源
    最近更新 更多