【问题标题】:async/await continuing too soon异步/等待太快继续
【发布时间】:2022-01-27 16:22:14
【问题描述】:

所以我基本上试图将过滤器进程的调用延迟 1.5 秒,以允许用户键入多个击键以防万一。如果键入了新的击键,则取消先前等待的任务并开始等待新的任务:

System.Threading.CancellationTokenSource token = new System.Threading.CancellationTokenSource();

private async void MyTextBox_TextChanged(object sender, TextChangedEventArgs e)
{
  token.Cancel();

  await System.Threading.Tasks.Task.Delay(1500, token.Token);
  this.filterText = (sender as TextBox).Text;
  (this.Resources["CVS"] as CollectionViewSource).View.Refresh();

  //Earlier I had tried this variant too:
  //System.Threading.Tasks.Task.Delay(500, token.Token).ContinueWith(_ =>
  //{
  //  this.filterText = (sender as TextBox).Text;
  //  (this.Resources["CVS"] as CollectionViewSource).View.Refresh();
  //});
}

但过滤过程(View.Refresh() 行)在第一次击键时立即命中,无需等待。我的印象是,在令牌上调用 Cancel 会杀死 Delay() 并因此也终止继续任务,然后再种植下一个,但显然这个方案不起作用。

我错过了什么?

【问题讨论】:

  • 您取消了用于取消延迟的令牌。所以没有发生延迟。
  • 是的,但是令牌是一次性的吗?如果没有,请查看我在取消前一个任务后开始新任务。
  • 取消后,你有一个取消的Token。您需要一个新的 CancellationTokenSource。
  • 您的 ContinueWith 不使用令牌,因此对其没有影响。使用实际使用令牌的 ContinueWith 重载。
  • token.Token token 不是令牌,而是CancellationTokenSource。使用正确的命名,因此很清楚它是什么以及它的作用。请注意,CancellationTokenSource 是否有 Reset()UnCancel() 方法?不,你需要一个新的。编辑:我只看到it has a TryReset。不过,这可能不是最好的用例。

标签: c# async-await task-parallel-library


【解决方案1】:

处理此问题的正确方法不是使用Task.Delay 和异常(因为异常是针对特殊情况的),而是使用TimerTimer.Elapsed 事件。

例如

using Timer = System.Windows.Forms.Timer;

private readonly Timer timer = new Timer();
private static string newText = "";

public Form1()
{
    timer.Interval = 1500;
    timer.Tick += OnTimedEvent;
}

private void MyTextBox_TextChanged(object sender, EventArgs e)
{
    timer.Stop(); // sets the time back to 0
    newText = (sender as TextBox).Text; // sets new text
    timer.Start(); // restarts the timer
}

private void OnTimedEvent(Object source, EventArgs e)
{
    filterText = newText;
    (Resources["CVS"] as CollectionViewSource).View.Refresh();
}

(不确定这是否 100% 正确,但您明白了要点。)


与cmets讨论有关的老sn-p。

正如帖子所说:这不是必需的,因为Task.Delay 会将听众链接到CancellationToken,因此.Cancel() 将阻塞,直到所有听众都听到它为止。

using System.Threading;
using System.Threading.Tasks;

private CancellationTokenSource cts = new CancellationTokenSource();
private Task delayTask;

private async void TenantsFilter_TextChanged(object sender, TextChangedEventArgs e)
{
  cts.Cancel();
  if (delayTask != null) {
    try{await delayTask;}
    catch(TaskCanceledException){}
  }
  cts = new CancellationTokenSource();

  try
  {
    delayTask = Task.Delay(1500, cts.Token);
    await delayTask;
    this.filterText = (sender as TextBox).Text;
    (this.Resources["CVS"] as CollectionViewSource).View.Refresh();
  }
  catch(TaskCanceledException)
  {
  }
}

【讨论】:

  • 好的。我花了更多时间阅读how CancellationTokenSource works。如果我的理解是正确的,我们代码中的cts.Cancel 调用会阻塞并等待所有注册的回调完成。因此,理想情况下,这里不应该存在竞争条件。
  • @dotNET 哇,很棒的发现。我今天学到了一些重要的东西。我明天会删除这个答案
  • 为什么是delayTask.Wait(); 而不是await delayTask;
  • 所以您认为async 方法中的每个await 都会导致一个额外的状态机?这不是它的工作原理。无论async 方法中有多少awaits,每个async 方法都会产生一个状态机。出于这个原因,避免使用await,并冒着通过在可能尚未完成的任务上调用.Wait() 来阻塞UI 线程的风险,是不合理的。
  • @TheodorZoulias 啊,谢谢。遗憾的是,我在 async/await 方面没有太多经验,因为我公司的技术主管禁止使用它们(他不认识它们,不信任它们,也不想学习它们……他很奇怪)
【解决方案2】:

如果这对任何人都有帮助,那么以下内容对我来说是正确的。我的错误是我错误地认为 CancellationTokenSource 是一个信号设备并且可以多次使用。显然不是这样的:

private System.Threading.CancellationTokenSource cts = new System.Threading.CancellationTokenSource();
private async void TenantsFilter_TextChanged(object sender, TextChangedEventArgs e)
{
  cts.Cancel();
  cts = new System.Threading.CancellationTokenSource();

  try
  {
    await System.Threading.Tasks.Task.Delay(1500, cts.Token);
    this.filterText = (sender as TextBox).Text;
    (this.Resources["CVS"] as CollectionViewSource).View.Refresh();
  }
  catch(System.Threading.Tasks.TaskCanceledException ee)
  {
  }
}

把它贴在这里是为了我自己的记录,只是为了让其他人检查我仍然没有做错任何事情。

【讨论】:

  • 考虑到这一点,我不确定这是 100% 线程安全的。 cts 在您替换它时被另一个任务/线程引用。如果另一个线程还没有看到取消(这可能发生,因为调度不确定),则可能存在数据竞争。在替换之前,您需要确保其他任务已完成。
  • @JHBonarius: 嗯...此外,这可以由用户快速连续调用。你有什么建议?可能是监视器或互斥锁?
  • 我只是在大声思考,因为这不是一件小事。但也许在等待之前将 Task.Delay 对象分配给私有字段。然后你可以.Wait()就可以了。
  • @JHBonarius - 我不确定你认为额外的任务会做什么。在应用await 时,已经调用了对Task.Delay 的调用,评估cts.Token 的值并将其存储在内部。 await 只是使用 Task.Delay 方法 returned 的任何等待。
  • 对于懒惰的未来读者,我也在此处粘贴相同的评论。好的。我花了更多时间阅读how CancellationTokenSource works。如果我的理解是正确的,我们代码中的 cts.Cancel 调用会阻塞并等待所有注册的回调完成。因此,理想情况下,这里不应该存在竞争条件。
猜你喜欢
  • 1970-01-01
  • 2017-11-14
  • 2019-03-02
  • 1970-01-01
  • 2019-07-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多