【问题标题】:Correctly cancel async operation and fire it again正确取消异步操作并再次触发
【发布时间】:2014-01-27 20:55:40
【问题描述】:

如何处理用户可能多次点击按钮的情况,这会调用长时间运行的异步操作。

我的想法是首先检查异步操作是否正在运行,取消它并再次触发它。

到目前为止,我已经尝试使用 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() 时,我将加载指示器设置为可见,当此方法结束时,我隐藏加载指示器。所以现在当我开始新的异步操作后取消之前的异步操作时,我的加载指示器被设置为隐藏。当“旧”方法结束时,如果还有另一种异步方法仍在运行,我应该如何跟踪。

【问题讨论】:

  • 如何它不起作用。不要只是说 它不起作用。当然,吞下所有异常通常是个坏主意。不要那样做。当你这样做时,你永远不会知道出了什么问题。
  • 当然,旧的仍然可以运行。您永远不会等到前一个操作完成后再继续。如果在请求取消和任务完成之间有一段时间,那么将有两个作业在这段时间内运行。
  • @devha,这取决于您如何显示进度。是对话吗?
  • 再次更新问题:)
  • @devha,更新了答案。

标签: c# asynchronous windows-phone-8 async-await cancellationtokensource


【解决方案1】:

我想借此机会完善some related code。在您的情况下,可以像下面这样使用它。

注意,如果挂起操作的前一个实例失败(抛出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;
    }
}

【讨论】:

    【解决方案2】:

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

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

    【讨论】:

    • 这如何帮助确保任务的前一个实例已被完全取消并且不与新实例重叠?
    • 我使用 ThrowIfCancellationRequested 取消异步操作。
    【解决方案3】:

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2015-02-11
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-11-14
      • 2015-01-09
      相关资源
      最近更新 更多