正确取消异步操作并重新触发它

3
如何处理用户可能会多次点击按钮并触发长时间运行的异步操作的情况。
我的想法是首先检查异步操作是否正在运行,取消它并重新启动它。
到目前为止,我已经尝试使用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()时,我设置加载指示器可见,当此方法结束时,我隐藏加载指示器。因此,现在当之前的异步操作在我启动新操作后被取消时,我的加载指示器被设置为隐藏。当“旧”操作结束时,我应该如何跟踪是否还有另一个异步方法正在运行。


它为什么不工作?不要只是说它不工作。当然,吞咽所有异常通常是一个坏主意。不要这样做。这样做时,你永远不会知道问题出在哪里。 - Servy
1
当然,旧的任务仍然可以运行。在继续之前,您从未等待上一个操作完成。如果在请求取消和任务完成之间有时间,那么在此期间将有两个作业正在运行。 - Servy
@devha,这取决于您如何显示进度。它是一个对话框吗? - noseratio - open to work
再次更新问题 :) - devha
@devha,已更新答案。 - noseratio - open to work
3个回答

3

我想有机会完善一些相关的代码。在你的情况下,它可以像下面这样使用。

请注意,如果挂起操作的先前实例失败了(抛出任何除OperationCanceledException之外的内容),您仍将看到其错误消息。此行为可以很容易地更改。

它只在操作结束时仍然是任务的最新实例时隐藏进度UI:if (thisTask == _draw.PendingTask) _progressWindow.Hide();

此代码不是线程安全的(_draw.RunAsync不能同时调用),并且设计为从UI线程调用。

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

0
为什么不遵循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;
}

0

调用 cts.Cancel() 不会自动停止任务。您的任务需要主动检查是否已请求取消。您可以像这样做:

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

        DoSomeStuff();
    }
}

这如何确保先前的任务实例已被完全取消,不会与新任务重叠? - noseratio - open to work
我使用ThrowIfCancellationRequested来取消异步操作。 - devha

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接