Правильно отмените асинхронную операцию и запустите ее снова

Как обрабатывать случай, когда пользователь может нажать кнопку, которая запускает длительную асинхронную операцию, несколько раз.

Моя идея состояла в том, чтобы сначала проверить, выполняется ли асинхронная операция, отменить ее и запустить снова.

До сих пор я пытался создать такую ​​функциональность, используя CancellationTokenSource, но она не работает должным образом. Иногда выполняется две асинхронные операции, поэтому "старые" асинхронные операции еще не отменяются, когда я запускаю новую, и это смешивает обработку результатов.

Какие-нибудь предложения или примеры, как обращаться с этим случаем?

public async void Draw()
{
    bool result = false;

    if (this.cts == null)
    {
        this.cts = new CancellationTokenSource();

        try
        {
            result = await this.DrawContent(this.TimePeriod, this.cts.Token);
        }
        catch (Exception ex)
        {}
        finally
        {
            this.cts = null;
        }
    }

    else
    {
        this.cts.Cancel();
        this.cts = new CancellationTokenSource();

        try
        {
            result = await this.DrawContent(this.TimePeriod, this.cts.Token);
        }
        catch (Exception ex)
        {}
        finally
        {
            this.cts = null;
        }
    }

}

РЕДАКТИРОВАТЬ: В конце концов, я думаю, это не плохо, что за короткое время выполняются две асинхронные операции (когда запускается новая, но старая еще не отменена).

Настоящая проблема здесь в том, как я показываю прогресс для конечного пользователя. Когда старая асинхронная операция завершается, она скрывает индикатор выполнения от конечного пользователя, но вновь запущенная асинхронная операция все еще выполняется.

РЕДАКТИРОВАТЬ 2: Внутри DrawContent(...) Я использую ThrowIfCancellationRequested, поэтому отмена запущенного задания, кажется, работает нормально.

О показе прогресса. Когда вызывается Draw(), я устанавливаю индикатор загрузки видимым, а когда этот метод заканчивается, я скрываю индикатор загрузки. Так что теперь, когда предыдущая асинхронная операция отменяется после запуска новой, мой индикатор загрузки становится скрытым. Как я должен следить, если еще один асинхронный метод все еще работает, когда "старый" заканчивается.

3 ответа

Решение

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

Обратите внимание, что если предыдущий экземпляр ожидающей операции завершился неудачно (выдается что-то кроме OperationCanceledException), вы все равно увидите сообщение об ошибке. Это поведение может быть легко изменено.

Он только скрывает пользовательский интерфейс выполнения, если к концу операции, если это все еще самый последний экземпляр задачи: if (thisTask == _draw.PendingTask) _progressWindow.Hide();

Этот код не является потокобезопасным, как есть (_draw.RunAsync не может быть вызван одновременно) и предназначен для вызова из потока пользовательского интерфейса.

Window _progressWindow = new Window();

AsyncOp _draw = new AsyncOp();

async void Button_Click(object s, EventArgs args)
{
    try
    {
        Task thisTask = null;
        thisTask = _draw.RunAsync(async (token) =>
        {
            var progress = new Progress<int>(
                (i) => { /* update the progress inside progressWindow */ });

            // show and reset the progress
            _progressWindow.Show();
            try
            {
                // do the long-running task
                await this.DrawContent(this.TimePeriod, progress, token);
            }
            finally
            {
                // if we're still the current task,
                // hide the progress 
                if (thisTask == _draw.PendingTask)
                    _progressWindow.Hide();
            }
        }, CancellationToken.None);
        await thisTask;
    }
    catch (Exception ex)
    {
        while (ex is AggregateException)
            ex = ex.InnerException;
        if (!(ex is OperationCanceledException))
            MessageBox.Show(ex.Message);
    }
}

class AsyncOp
{
    Task _pendingTask = null;
    CancellationTokenSource _pendingCts = null;

    public Task PendingTask { get { return _pendingTask; } }

    public void Cancel()
    {
        if (_pendingTask != null && !_pendingTask.IsCompleted)
            _pendingCts.Cancel();
    }

    public Task RunAsync(Func<CancellationToken, Task> routine, CancellationToken token)
    {
        var oldTask = _pendingTask;
        var oldCts = _pendingCts;

        var thisCts = CancellationTokenSource.CreateLinkedTokenSource(token);

        Func<Task> startAsync = async () =>
        {
            // await the old task
            if (oldTask != null && !oldTask.IsCompleted)
            {
                oldCts.Cancel();
                try
                {
                    await oldTask;
                }
                catch (Exception ex)
                {
                    while (ex is AggregateException)
                        ex = ex.InnerException;
                    if (!(ex is OperationCanceledException))
                        throw;
                }
            }
            // run and await this task
            await routine(thisCts.Token);
        };

        _pendingCts = thisCts;

        _pendingTask = Task.Factory.StartNew(
            startAsync,
            _pendingCts.Token,
            TaskCreationOptions.None,
            TaskScheduler.FromCurrentSynchronizationContext()).Unwrap();

        return _pendingTask;
    }
}

Почему бы не следовать шаблону BackgroundWorker и выйти из цикла в DrawContent?

private bool _cancelation_pennding=false;
private delegate DrawContentHandler(TimePeriod period, Token token)
private DrawContentHandler _dc_handler=null;

.ctor(){
    this._dc_handler=new DrawContentHandler(this.DrawContent)
}
public void CancelAsync(){
    this._cancelation_pennding=true;
}
public void Draw(){
    this._dc_handler.BeginInvoke(this.TimePeriod, this.cts.Token)
}
private void DrawContent(TimePeriod period, Token token){
    loop(){
        if(this._cancelation_pennding)
        {
            break;
        }

        //DrawContent code here
    }
    this._cancelation_pennding=false;
}

Вызов cts.Cancel() не остановит задачу автоматически. Ваша задача должна активно проверять, была ли отменена заявка. Вы можете сделать что-то вроде этого:

public async Task DoStuffForALongTime(CancellationToken ct)
{
    while (someCondition)
    {
        if (ct.IsCancellationRequested)
        {
            return;
        }

        DoSomeStuff();
    }
}
Другие вопросы по тегам