异步/等待中的重入问题?

9

我有一个按钮,它有一个async处理程序,该处理程序调用了一个异步方法并使用了await关键字。下面是代码示例:

private async void Button1_OnClick(object sender, RoutedEventArgs e)
{
    await IpChangedReactor.UpdateIps();
}

下面是 IpChangedReactor.UpdateIps() 的样子:

public async Task UpdateIps()
{
    await UpdateCurrentIp();
    await UpdateUserIps();
}

它是异步的全部。现在我有一个DispatcherTimer,在其tick事件中重复调用await IpChangedReactor.UpdateIps。假设我点击了按钮。现在,事件处理程序在UpdateIps上等待并返回到调用方,这意味着WPF将继续执行其他操作。同时,如果计时器触发,则会再次调用UpdateIps,现在两个方法都将同时运行。所以我认为它类似于使用2个线程。可能会发生竞争条件吗?(我的一部分说不会,因为它们都在同一个线程中运行。但这很令人困惑)
我知道异步方法不一定在单独的线程上运行。然而,在这种情况下,它非常令人困惑。
如果我在这里使用同步方法,它将按预期工作。计时器tick事件将仅在第一次调用完成后运行。
有人能给我启示吗?

6
你正在努力寻找的术语是“可重入性”:“在计算机领域,如果一个计算机程序或子程序能够在执行过程中被安全地中断,并且可以在之前的调用完成执行之前再次被调用(即“重新进入”),则称其为可重入的。” - Scott Chamberlain
2个回答

6

由于这两个调用都在UI线程上运行,因此从传统意义上来说,该代码是“线程安全”的 - 不会出现任何异常或损坏的数据。

然而,是否存在逻辑竞争条件呢?当然可以。您可以轻松地拥有此流程(或任何其他流程):

UpdateCurrentIp() - button
UpdateCurrentIp() - Timer
UpdateUserIps() - Timer
UpdateUserIps() - button

从方法名称来看,似乎并不是真正的问题,但这取决于这些方法的实际实现。

通常情况下,您可以通过使用SemaphoreSlimAsyncLock如何保护可能在多线程或异步环境中使用的资源?)来同步调用以避免这些问题:

using (await _asyncLock.LockAsync())
{
    await IpChangedReactor.UpdateIps();
}

在这种情况下,似乎只要避免在当前正在运行更新时开始新的更新就足够了。
if (_isUpdating) return;

_isUpdating = true;
try
{
    await IpChangedReactor.UpdateIps();
}
finally
{
    _isUpdating = false;
}

如果您的计时器需要比计时器的滴答时间更长的时间才能完成工作,使用锁定可能并不合适。您只需在前一个调用完成之前不触发新的调用即可,否则您将会得到一个不断增加的积压任务队列。 - Servy
@i3arnon 代码可能有许多可能的行为。如果以固定间隔运行代码,但如果上一次迭代仍在运行,则不会触发它,这可能是正确的。如果这是OP想要的语义,那么你的代码就无法提供它。你建议在前一次执行结束和下一次开始之间使用固定间隔运行代码。如果这是他想要的,那很好,如果不是,那就无法实现。总的来说,两者都没有对错之分,但它们是不同的行为。你可以选择符合你要求的那个。 - Servy
@Servy,1. 我的代码确实提供了OP想要的东西。2. 当您实现刷新机制时,等待迭代之间的时间而不是盲目使用计时器的间隔绝对是一种“更好的做法”。3. 您不同意的评论不是将答案投票反对的好理由(假设它是您的答案)。 - i3arnon
这两种机制在不同情况下都是合理的。在某些情况下,一种机制更好,在另一些情况下另一种更好。没有一种比另一种普遍更好。将所需语义从要求中更改并不完全提供OP所请求的内容。 - Servy
“仅仅加锁”的语义可能是错误的。这就是我一开始就告诉你的。是你在你的回答中建议的。我不主张仅仅加锁。只有在前一个处理程序没有运行时才触发计时器的处理程序可能是可以的。提议在任务结束和下一个任务开始之间等待固定时间可能有效,但这是与OP所述要求的偏离,因此他应该只在想要或接受更改的情况下进行更改。 - Servy
显示剩余7条评论

4
我可以想到处理这个问题的几种方法。

1 不处理

就像i3arnon所说,同时运行多个调用方法可能不是问题。这完全取决于更新方法的实现方式。就像你所写的那样,在真正的多线程并发中面临的问题非常相似。如果同时运行多个异步操作对这些方法来说不是问题,那么您可以忽略重入问题。

2 阻塞计时器,并等待正在运行的任务完成

当您知道有异步任务正在运行时,可以禁用计时器或阻止对事件处理程序的调用。您可以使用简单的状态字段或任何类型的锁定/信号原语来实现此操作。这确保您一次只运行一个操作。

3 取消任何正在进行的异步操作

如果要取消已经在运行的任何异步操作,可以使用取消令牌来停止它们,然后启动新操作。请参考此链接如何取消await中的任务?

这将有意义,如果操作需要很长时间才能完成,并且您希望避免花费时间来完成已经“过时”的操作。

4 将请求排队

如果实际运行所有更新很重要,并且您需要同步,则可以将任务排队,并逐个完成它们。如果您选择这种方法,请考虑添加某种形式的反压处理...


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