Pergunta

How to handle case, where user might hit the button, which invokes long running async operation, multiple time.

My idea was first check if the async operation is running, cancel it and fire it again.

So far I have tried to build this kind of functionality using CancellationTokenSource, but it is not working as expected. Some times there is two async operations running, so the "old" async oprations is not cancelled yet when I start new one and this mixes up the resul handling.

Any suggestions or examples how to handle this kind of case?

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

}

EDIT: In the end, I think it is not bad that there is two async operations running in short time (when the new is fired but the old one is not yet cancelled).

The real problem here is how I display the progress for enduser. As when the old async operation ends, it hides the progress indicator from enduser but the newly fired async operation is still running.

EDIT2: Inside DrawContent(...) I use ThrowIfCancellationRequested, so cancelling the running task seems to work ok.

About progress display. When the Draw() is called, I set loading indicator visible and when this method ends, I hide loading indicator. So now when the previous async operation is cancelled after I start new one, my loading indicator is set hidden. How should I keep track if there is another async method still running when the "old" one ends.

Foi útil?

Solução

I'd like to take a chance to refine some related code. In your case, it can be used like below.

Note, if the previous instance of the pending operation has failed (thrown anything other than OperationCanceledException), you'll still see an error message for it. This behavior can be easily changed.

It only hides the progress UI if by the end of the operation if it's still the most recent instance of the task: if (thisTask == _draw.PendingTask) _progressWindow.Hide();

This code is not thread-safe as is (_draw.RunAsync can't be called concurrently), and is designed to be called from a UI thread.

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

Outras dicas

Calling cts.Cancel() will not automatically stop a Task. Your Task needs to actively check whether cancellation has been requested. You can do something like this:

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

        DoSomeStuff();
    }
}

Why not follow the BackgroundWorker pattern and break out of the loop in 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;
}
Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top