Как отменить задачу без исключения? - PullRequest
0 голосов
/ 10 мая 2019

Мне нужно выполнить задачу типа LongRunning после задержки. Каждое задание можно отменить. Я предпочитаю TPL с cancellationToken.

Поскольку моя задача долго выполняется, и перед ее запуском она должна быть помещена в словарь, поэтому я должен использовать 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 голосов
/ 11 мая 2019

Причиной несогласованного поведения является принципиально неправильное использование асинхронного делегата в первом случае.Конструкторы Task просто не получают Func<Task>, и ваш асинхронный делегат всегда интерпретируется как async void, а не async Task в случае использования с конструктором.Если исключение вызывается в методе async Task, оно перехватывается и помещается в объект Task, что неверно для метода async void, в этом случае исключение просто выпадает из метода в контекст синхронизации и переходит вкатегория необработанных исключений (вы можете ознакомиться с подробностями в этой статье Стивена Клири).Итак, что происходит в случае использования конструктора: задача, которая должна инициировать асинхронный поток, создается и запускается.Как только он достигает точки, когда Task.Delay(...) возвращает обещание, задача завершается, и она больше не имеет отношения ни к чему, что происходит в продолжении Task.Delay (вы можете легко проверить в отладчике, установив точку останова на value.cts.Cancel(), что объект задачи всловарь _tasks имеет статус RanToCompletetion, хотя делегат задачи по-прежнему работает).Когда запрашивается отмена, исключение возникает в методе Task.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.

0 голосов
/ 10 мая 2019

Я получил следующее решение:

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