Correctly cancel async operation and fire it again
Asked Answered
E

3

3

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.

Every answered 27/1, 2014 at 20:55 Comment(4)
how is it not working. Don't just say that it's not working. Of course, swallowing all exceptions is a generally bad idea. Don't do that. You'll never know what's wrong when you do that.Mallis
Well of course the old one can still be running. You never at any point wait until the previous operation finishes before continuing on. If there is time between when cancellation is requested and when the task finishes, there will be two jobs running for that time.Mallis
@devha, it depends on how you display the progress. Is it a dialog?Deese
Update question again :)Every
D
3

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;
    }
}
Deese answered 27/1, 2014 at 22:4 Comment(0)
O
0

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();
    }
}
Oology answered 28/1, 2014 at 0:17 Comment(2)
How does this help to make sure the previous instance of the task has been fully cancelled and doesn't overlap with the new one?Deese
I use ThrowIfCancellationRequested to cancel async operation.Every
S
0

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;
}
Slash answered 7/2, 2014 at 20:10 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.