2015-09-11 29 views
2

美好的一天!我正在为WinForms UI编写一个帮助程序库。使用TPL异步/的await机制启动,得到了一个问题,这样的代码示例:如何处理TPL中的任务取消

private SynchronizationContext _context; 

    public void UpdateUI(Action action) 
    { 
     _context.Post(delegate { action(); }, null); 
    } 


    private async void button2_Click(object sender, EventArgs e) 
    { 

     var taskAwait = 4000; 
     var progressRefresh = 200; 
     var cancellationSource = new System.Threading.CancellationTokenSource(); 

     await Task.Run(() => { UpdateUI(() => { button2.Text = "Processing..."; }); }); 

     Action usefulWork =() => 
     { 
      try 
      { 
       Thread.Sleep(taskAwait); 
       cancellationSource.Cancel(); 
      } 
      catch { } 
     }; 
     Action progressUpdate =() => 
     { 
      int i = 0; 
      while (i < 10) 
      { 
       UpdateUI(() => { button2.Text = "Processing " + i.ToString(); }); 
       Thread.Sleep(progressRefresh); 
       i++; 
      } 
      cancellationSource.Cancel(); 
     }; 

     var usefulWorkTask = new Task(usefulWork, cancellationSource.Token); 
     var progressUpdateTask = new Task(progressUpdate, cancellationSource.Token); 

     try 
     { 
      cancellationSource.Token.ThrowIfCancellationRequested(); 
      Task tWork = Task.Factory.StartNew(usefulWork, cancellationSource.Token); 
      Task tProgress = Task.Factory.StartNew(progressUpdate, cancellationSource.Token); 
      await Task.Run(() => 
      { 
       try 
       { 
        var res = Task.WaitAny(new[] { tWork, tProgress }, cancellationSource.Token);       
       } 
       catch { } 
      }).ConfigureAwait(false); 
     } 
     catch (Exception ex) 
     { 
     } 
     await Task.Run(() => { UpdateUI(() => { button2.Text = "button2"; }); }); 
    } 

基本上,这个想法是运行两个并行任务 - 一个是,比如说,进度条或任何更新和排序的超时控制器,另一个是长时间运行的任务本身。无论哪个任务先完成取消另一个任务。所以,取消“进度”任务应该没有问题,因为它有一个循环,在该循环中我可以检查任务是否被标记为取消。问题在于长时间运行的问题。它可以是Thread.Sleep()或SqlConnection.Open()。当我运行CancellationSource.Cancel()时,长时间运行的任务继续工作,不会取消。在超时之后,我对长时间运行的任务或任何可能导致的结果不感兴趣。
由于混乱的代码示例可能会提示,我尝试了一堆变体,但都没有给出我想要的效果。像Task.WaitAny()这样的东西冻结UI ...有没有办法让这种取消工作,或者甚至可能是一种不同的方法来编码这些东西?

UPD:

public static class Taskhelpers 
{ 
    public static async Task<T> WithCancellation<T>(this Task<T> task, CancellationToken cancellationToken) 
    { 
     var tcs = new TaskCompletionSource<bool>(); 
     using (cancellationToken.Register(s => ((TaskCompletionSource<bool>)s).TrySetResult(true), tcs)) 
     { 
      if (task != await Task.WhenAny(task, tcs.Task)) 
       throw new OperationCanceledException(cancellationToken); 
     } 
     return await task; 
    } 
    public static async Task WithCancellation(this Task task, CancellationToken cancellationToken) 
    { 
     var tcs = new TaskCompletionSource<bool>(); 
     using (cancellationToken.Register(s => ((TaskCompletionSource<bool>)s).TrySetResult(true), tcs)) 
     { 
      if (task != await Task.WhenAny(task, tcs.Task)) 
       throw new OperationCanceledException(cancellationToken); 
     } 
     await task; 
    } 
} 

.....

 var taskAwait = 4000; 
     var progressRefresh = 200; 
     var cancellationSource = new System.Threading.CancellationTokenSource(); 
     var cancellationToken = cancellationSource.Token; 

     var usefulWorkTask = Task.Run(async() => 
     { 
      try 
      { 
       System.Diagnostics.Trace.WriteLine("WORK : started"); 

       await Task.Delay(taskAwait).WithCancellation(cancellationToken); 

       System.Diagnostics.Trace.WriteLine("WORK : finished"); 
      } 
      catch (OperationCanceledException) { } // just drop out if got cancelled 
      catch (Exception ex) 
      { 
       System.Diagnostics.Trace.WriteLine("WORK : unexpected error : " + ex.Message); 
      } 
     }, cancellationToken); 

     var progressUpdatetask = Task.Run(async() => 
     { 
      for (var i = 0; i < 25; i++) 
      { 
       if (!cancellationToken.IsCancellationRequested) 
       { 
        System.Diagnostics.Trace.WriteLine("==== : " + i.ToString()); 
        await Task.Delay(progressRefresh); 
       } 
      } 
     },cancellationToken); 

     await Task.WhenAny(usefulWorkTask, progressUpdatetask); 

     cancellationSource.Cancel(); 

通过修改for (var i = 0; i < 25; i++)限制i我模仿长期运行的任务是否进度任务或以其他方式之前完成。按需要工作。 WithCancellation辅助方法完成这项工作,尽管现在两种'嵌套'Task.WhenAny看起来很可疑。

回答

4

我与保罗的回答所有的点一致 - 即利用先进的解决方案(Task.Run而不是Task.Factory.StartNewProgress<T>的最新进展,而不是手动发布到SynchronizationContext,Task.WhenAny而不是Task.WaitAny用于异步代码)。

但是,为了回答实际问题:

当我运行CancellationSource.Cancel(),长时间运行的任务继续工作并不会取消。超时后,我没有兴趣在长期运行的任务或任何可能导致

有两个环节进行。

  • 如何编写响应取消请求的代码?
  • 如何编写在取消后忽略任何响应的代码?

注意与第一部分涉及取消操作,而第二部分实际处理取消等待的操作完成

首先要做的是:支持操作本身取消。对于CPU限制的代码(即运行一个循环),定期调用token.ThrowIfCancellationRequested()。对于I/O绑定代码,最好的选择是将token传递给下一个API层 - 大多数(但不是全部)I/O API可以(应该)取消取消令牌。如果这不是一个选项,那么您可以选择忽略取消,或者您可以使用token.Register注册取消回叫。有时您可以从您的Register回调中调用单独的取消方法,并且有时可以通过从回调中丢弃对象来使其工作(这种方法通常是有效的,因为长期以来Win32 API传统取消了所有的I/O当该手柄关闭时处理)。不过,我不确定这是否适用于SqlConnection.Open

接下来,取消等待。这是一个比较简单的,如果你只是想取消等待由于超时

await Task.WhenAny(tWork, tProgress, Task.Delay(5000)); 
+0

'等待Task.WhenAny(tWork,tProgress,Task.Delay(5000));'非常简单直接! –

+0

@SergeMisnik:是的。如果你想要真正观察一个真正的取消标记,它会变得相当复杂一点,但如果它只是一个超时,那么它很简单。 :) –

1

我认为你需要在progressUpdate行动中检查IsCancellationRequested

至于如何做到你想要的,this blog讨论了扩展方法WithCancellation,这将使你停止等待你长时间运行的任务。

+3

在这个博客的一个重要概念是,你*不*取消不支持取消操作。你只能停止等待回应。在某些情况下,您可以放弃任何需要很长时间的事情,例如通过中止线程或关闭数据库连接。在其他情况下,您不能,例如,您可以中止REST或Web服务请求。您只能停止等待服务器响应。虽然服务器不会知道你已经停止等待 –

+0

'WithCancellation'方法实际上效果很好 –

2

当您在button2_Click上编写类似await Task.Run(() => { UpdateUI(() => { button2.Text = "Processing..."; }); });的东西时,您将从UI线程调度操作到线程轮询线程,该线程轮询线程将操作发布到UI线程。如果你直接调用动作,它会更快,因为它不会有两个上下文切换。

ConfigureAwait(false)导致同步上下文不被捕获。我不应该在UI方法中使用它,因为最可靠的是,你需要为延续做一些UI工作。

除非您绝对有理由,否则不应使用Task.Factory.StartNew而不是Task.Run。见thisthis

对于进度更新,请考虑使用Progress<T> class,因为它捕获同步上下文。

也许你应该尝试这样的事:

private async void button2_Click(object sender, EventArgs e) 
{ 
    var taskAwait = 4000; 
    var cancellationSource = new CancellationTokenSource(); 
    var cancellationToken = cancellationSource.Token; 

    button2.Text = "Processing..."; 

    var usefullWorkTask = Task.Run(async() => 
     { 
      try 
      { 
       await Task.Dealy(taskAwait); 
      } 
      catch { } 
     }, 
     cancellationToken); 

    var progress = new Progress<imt>(i => { 
     button2.Text = "Processing " + i.ToString(); 
    }); 

    var progressUpdateTask = Task.Run(async() => 
     { 
      for(var i = 0; i < 10; i++) 
      { 
       progress.Report(i); 
      } 
     }, 
     cancellationToken); 

    await Task.WhenAny(usefullWorkTask, progressUpdateTask); 

    cancellationSource.Cancel(); 
} 
+0

我刚才读了S.Toub的文章,但最近无法找到它们重新阅读。所以,必须做一个随机的尝试错误的方法...不是一个好的学习方式。我会试一试你的代码。 –