Как обрабатывать отмену задачи в TPL

Добрый день! Я пишу вспомогательную библиотеку для пользовательского интерфейса WinForms. Начал использовать механизм TPL async / await и столкнулся с проблемой с таким примером кода:

    private SynchronizationContext _context;

    public void UpdateUI(Action action)
    {
        _context.Post(delegate { action(); }, null);
    }


    private async void button2_Click(object sender, EventArgs e)
    {

        var taskAwait = 4000;
        var progressRefresh = 200;
        var cancellationSource = new System.Threading.CancellationTokenSource();

        await Task.Run(() => { UpdateUI(() => { button2.Text = "Processing..."; }); });

        Action usefulWork = () =>
        {
            try
            {
                Thread.Sleep(taskAwait);
                cancellationSource.Cancel();
            }
            catch { }
        };
        Action progressUpdate = () =>
        {
            int i = 0;
            while (i < 10)
            {
                UpdateUI(() => { button2.Text = "Processing " + i.ToString(); });
                Thread.Sleep(progressRefresh);
                i++;
            }
            cancellationSource.Cancel();
        };

        var usefulWorkTask = new Task(usefulWork, cancellationSource.Token);
        var progressUpdateTask = new Task(progressUpdate, cancellationSource.Token);

        try
        {
            cancellationSource.Token.ThrowIfCancellationRequested();
            Task tWork = Task.Factory.StartNew(usefulWork, cancellationSource.Token);
            Task tProgress = Task.Factory.StartNew(progressUpdate, cancellationSource.Token);
            await Task.Run(() =>
            {
                try
                {
                    var res = Task.WaitAny(new[] { tWork, tProgress }, cancellationSource.Token);                        
                }
                catch { }
            }).ConfigureAwait(false);
        }
        catch (Exception ex)
        {
        }
        await Task.Run(() => { UpdateUI(() => { button2.Text = "button2"; }); });
    }

По сути, идея состоит в том, чтобы запустить две параллельные задачи: одна предназначена, скажем, для индикатора выполнения или любого другого обновления и своего рода контроллера тайм-аута, а другая - это сама длительная задача. Какая бы задача ни была завершена первой, предыдущая отменяет выполнение другой. Таким образом, не должно быть проблем с отменой задачи «прогресс», поскольку у нее есть цикл, в котором я могу проверить, помечена ли задача как отмененная. Проблема в том, что долго работает. Это может быть Thread.Sleep () или SqlConnection.Open (). Когда я запускаю CancellationSource.Cancel (), длительная задача продолжает работать и не отменяется. После тайм-аута меня не интересует длительная задача или что-то еще, к чему она может привести.
Как показывает пример загроможденного кода, я перепробовал кучу вариантов, и ни один из них не дал мне желаемого эффекта. Что-то вроде Task.WaitAny () замораживает пользовательский интерфейс ... Есть ли способ заставить эту отмену работать или может быть даже другой подход к кодированию этих вещей?

UPD:

public static class Taskhelpers
{
    public static async Task<T> WithCancellation<T>(this Task<T> task, CancellationToken cancellationToken)
    {
        var tcs = new TaskCompletionSource<bool>();
        using (cancellationToken.Register(s => ((TaskCompletionSource<bool>)s).TrySetResult(true), tcs))
        {
            if (task != await Task.WhenAny(task, tcs.Task))
                throw new OperationCanceledException(cancellationToken);
        }
        return await task;
    }
    public static async Task WithCancellation(this Task task, CancellationToken cancellationToken)
    {
        var tcs = new TaskCompletionSource<bool>();
        using (cancellationToken.Register(s => ((TaskCompletionSource<bool>)s).TrySetResult(true), tcs))
        {
            if (task != await Task.WhenAny(task, tcs.Task))
                throw new OperationCanceledException(cancellationToken);
        }
        await task;
    }
}

.....

        var taskAwait = 4000;
        var progressRefresh = 200;
        var cancellationSource = new System.Threading.CancellationTokenSource();
        var cancellationToken = cancellationSource.Token;

        var usefulWorkTask = Task.Run(async () =>
        {
            try
            {
                System.Diagnostics.Trace.WriteLine("WORK : started");

                await Task.Delay(taskAwait).WithCancellation(cancellationToken);

                System.Diagnostics.Trace.WriteLine("WORK : finished");
            }
            catch (OperationCanceledException) { }  // just drop out if got cancelled
            catch (Exception ex)
            {
                System.Diagnostics.Trace.WriteLine("WORK : unexpected error : " + ex.Message);
            }
        }, cancellationToken);

        var progressUpdatetask = Task.Run(async () =>
        {
            for (var i = 0; i < 25; i++)
            {
                if (!cancellationToken.IsCancellationRequested)
                {
                    System.Diagnostics.Trace.WriteLine("==== : " + i.ToString());
                    await Task.Delay(progressRefresh);
                }
            }
        },cancellationToken);

        await Task.WhenAny(usefulWorkTask, progressUpdatetask);

        cancellationSource.Cancel();

Изменяя for (var i = 0; i < 25; i++) предел i, я имитирую, завершается ли длительная задача до выполнения задачи прогресса или иначе. Работает по желанию. Свою работу выполняет WithCancellation вспомогательный метод, хотя два вида «вложенных» Task.WhenAny пока выглядят подозрительно.


person Serge Misnik    schedule 11.09.2015    source источник


Ответы (3)


Я согласен со всеми пунктами в ответе Пауло, а именно, использовать современные решения (Task.Run вместо Task.Factory.StartNew, Progress<T> для обновлений прогресса вместо ручной публикации в SynchronizationContext, Task.WhenAny вместо Task.WaitAny для асинхронного кода).

Но чтобы ответить на актуальный вопрос:

Когда я запускаю CancellationSource.Cancel (), длительная задача продолжает работать и не отменяется. После тайм-аута меня не интересует длительная задача или что-то еще, к чему она может привести.

Это состоит из двух частей:

  • Как написать код, отвечающий на запрос об отмене?
  • Как мне написать код, который игнорирует любые ответы после отмены?

Обратите внимание, что первая часть касается отмены операции, а вторая часть фактически касается отмены ожидания завершения операции.

Перво-наперво: поддержка отмены в самой операции. Для кода, привязанного к ЦП (т. Е. Выполнения цикла), периодически вызывайте token.ThrowIfCancellationRequested(). Для кода, связанного с вводом-выводом, лучший вариант - передать token на следующий уровень API - большинство (но не все) API ввода-вывода могут (должны) принимать токены отмены. Если это не вариант, вы можете либо игнорировать отмену, либо зарегистрировать обратный вызов отмены с помощью token.Register. Иногда существует отдельный метод отмены, который вы можете вызвать из своего обратного вызова Register, а иногда вы можете заставить его работать, удалив объект из обратного вызова (этот подход часто работает из-за давней традиции Win32 API отменять все операции ввода-вывода для ручку, когда эта ручка закрыта). Я не уверен, что это сработает для SqlConnection.Open.

Затем отмените wait. Это относительно просто, если вы просто хотите отменить ожидание из-за тайм-аута:

await Task.WhenAny(tWork, tProgress, Task.Delay(5000));
person Stephen Cleary    schedule 11.09.2015
comment
await Task.WhenAny(tWork, tProgress, Task.Delay(5000)); такой простой и понятный! - person Serge Misnik; 11.09.2015
comment
@SergeMisnik: Да. Это становится немного сложнее, если вы действительно хотите наблюдать за реальным токеном отмены, но если это просто тайм-аут, то все просто. :) - person Stephen Cleary; 11.09.2015

Когда вы пишете что-то вроде await Task.Run(() => { UpdateUI(() => { button2.Text = "Processing..."; }); }); на своем button2_Click, вы из потока пользовательского интерфейса планируете действие в потоке опроса, который отправляет действие в поток пользовательского интерфейса. Если бы вы вызывали действие напрямую, это было бы быстрее, потому что у него не было бы двух переключений контекста.

ConfigureAwait(false) приводит к тому, что контекст синхронизации не захватывается. Меня не следует использовать внутри методов пользовательского интерфейса, потому что вы наверняка захотите поработать с пользовательским интерфейсом над продолжением.

Вы не должны использовать Task.Factory.StartNew вместо Task.Run, если у вас нет для этого абсолютно никаких причин. См. это и это.

Для получения обновлений о ходе работы рассмотрите возможность использования класса Progress ‹T› class, потому что он захватывает контекст синхронизации.

Может, стоит попробовать что-то вроде этого:

private async void button2_Click(object sender, EventArgs e)
{
    var taskAwait = 4000;
    var cancellationSource = new CancellationTokenSource();
    var cancellationToken = cancellationSource.Token;
    
    button2.Text = "Processing...";
    
    var usefullWorkTask = Task.Run(async () =>
        {
            try
            {
                await Task.Dealy(taskAwait);
            }
            catch { }
        },
        cancellationToken);
    
    var progress = new Progress<imt>(i => {
        button2.Text = "Processing " + i.ToString();
    });

    var progressUpdateTask = Task.Run(async () =>
        {
            for(var i = 0; i < 10; i++)
            {
                progress.Report(i);
            }
        },
        cancellationToken);
        
    await Task.WhenAny(usefullWorkTask, progressUpdateTask);
    
    cancellationSource.Cancel();
}
person Paulo Morgado    schedule 11.09.2015
comment
Некоторое время назад я читал статьи С.Туба, но в последнее время не мог найти их, чтобы перечитать. Итак, пришлось применить случайный подход с попыткой и ошибкой ... Не лучший способ учиться. Я попробую твой код. - person Serge Misnik; 11.09.2015

Думаю, вам нужно проверить IsCancellationRequested в progressUpdate Action.

Что касается того, как делать то, что вы хотите, в этом блоге обсуждается метод расширения WithCancellation, который заставит вас перестать ждать выполнения своей длительной задачи.

person Ringil    schedule 11.09.2015
comment
Важная идея в этом блоге заключается в том, что вы не можете отменить операцию, которая не поддерживает отмену. Вам остается только перестать ждать ответа. В некоторых случаях вы можете прервать все, что занимает много времени, например, прервав поток или закрыв соединение с базой данных. В противном случае вы не можете, например, вы можете прервать запрос REST или веб-службы. Вы можете только перестать ждать ответа сервера. Сервер, хотя и не узнает, что вы перестали ждать - person Panagiotis Kanavos; 11.09.2015