异步任务和锁

10

我有一个元素列表需要由两个进程更新。第一个是UI线程(由用户控制),第二个是从Web服务检索信息的后台进程。

由于这个第二个进程受到I/O限制,似乎适合使用异步任务。这引出了一些问题:

  1. 由于异步任务不在单独的线程上运行,因此更新此列表时似乎不需要任何类型的锁定,对吗?

  2. 另一方面,我们可以假设异步任务永远不会在单独的线程上运行吗?

  3. 我正在谈论一个Windows Forms应用程序。也许将来我想将其作为控制台应用程序运行。据我所知,在控制台应用程序中,异步任务在单独的线程上运行。询问任务是否在单独的线程上运行的首选惯用语是什么?这样我就可以在必要时建立锁。

  4. 我不知道我是否真的需要锁定的事实使我想知道这是否是最佳设计。对于这种IO绑定的代码,坚持使用 Task.Run() 是否有意义?


1
你所说的process是指实际的独立窗口进程,还是其他什么?如果是,那么你如何在进程之间共享列表?我想这不是一个实际的进程,但你应该确认一下...它是用户通过与UI交互启动的东西吗?还是基于定时器运行的东西?这是非常重要的信息。 - MarcinJuraszek
它不是在计时器上运行的。它只是一个更新非常长列表的函数。由于更新每个元素都涉及连接到 Web 服务,因此更新整个列表可能需要几秒钟甚至几分钟。 - sapito
3个回答

9

由于异步任务不在单独的线程上运行,因此在更新此列表时似乎不需要任何类型的锁,对吗?

不能保证没有人使用Task.Run并将其方法标记为async。IO绑定的异步任务很可能没有在后台使用某种线程,但这并非总是如此。您不应该依赖它来确保代码的正确性。您可以通过将其包装到另一个不使用ConfigureAwait(false)async方法中来确保您的代码在UI线程上运行。您始终可以使用框架中提供的并发集合。也许ConcurrentBagBlockingCollection可以满足您的需求。

据我所知,在控制台应用程序中,异步任务在单独的线程上运行。询问任务是否在单独的线程上运行的首选语法是什么?

那是不正确的。仅因为它们在控制台应用程序中,async操作本身并不会在单独的线程上运行。简单地说,在控制台应用程序中,默认的TaskScheduler是默认的ThreadPoolTaskScheduler,它将在线程池线程上排队任何继续执行,因为控制台没有称为UI线程的实体。一般来说,这完全与SynchronizationContext有关 “我不知道是否真正需要锁的事实让我怀疑这是否是最佳设计。对于这种类型的IO绑定代码,坚持使用Task.Run()是否有意义?”
绝对不是。您不知道的事情就是您发布此问题的原因,也是我们试图帮助的原因。
在执行异步IO时,没有必要使用线程池线程。异步IO的整个重点在于,您可以释放调用线程以执行更多工作,同时处理请求。

2
我不同意关于并发和集合的观点。我同意没有保证你在UI线程上运行。然而,为了数据绑定,仅从UI线程访问集合是一个好的实践。这表明async I/O应该是纯的(因此线程安全),而Presenter/Controller/whatever应该是线程不安全的,并要求回调在UI线程上运行。但是,关于locking,async编程的重点是使编写单线程阻塞(无锁)程序更容易。 - Aron
我喜欢这个答案,但我认为Aron说得很好。@Aron:您是否建议使用Control.Invoke来更新列表和UI? - sapito
@Aron - 如果集合已经绑定到UI元素,那么我同意,实际上在这种情况下OP没有选择。否则,我认为没有理由限制自己只能在UI线程中操作集合并使用UI消息循环来访问集合。 - Yuval Itzchakov
@Yuval Itzchakov:实际上这个集合是“部分绑定”的。每次我更新其中一个元素时,都应该检查它是否属于正在显示的集合。如果是,我还必须更新UI。我认为即使没有所有元素都绑定,将所有更新都在UI线程上运行也是有意义的。 - sapito
@sapito 如果在这种情况下有意义,那就去做吧 :) 尽管我认为这可能不是处理没有涉及UI元素的情况的最佳解决方案。 - Yuval Itzchakov
@YuvalItzchakov:是的,我完全同意你的观点,显然我会考虑一种不同的方法来处理控制台应用程序(但是控制台应用程序现在并不是我的主要关注点)。 - sapito

6

由于异步任务不在单独的线程上运行,因此在更新此列表时似乎不需要任何类型的锁定,对吗?

确实如此。如果您遵循函数式模式(即每个后台操作将返回其结果,而不是更新共享数据),则此方法效果很好。因此,类似以下内容将正常工作:

async Task BackgroundWorkAsync() // Called from UI thread
{
  while (moreToProcess)
  {
    var item = await GetItemAsync();
    Items.Add(item);
  }
}

在这种情况下,GetItemAsync 的实现方式并不重要。它可以使用 Task.RunConfigureAwait(false) 等任何方法——BackgroundWorkAsync 在将项目添加到集合之前总是会与 UI 线程同步。

也许将来我想将其作为控制台应用程序运行。据我所知,在控制台应用程序中,异步任务在单独的线程上运行。

"异步任务" 本身根本不会“运行”。如果这令人困惑,我有一个 async intro 可能会有所帮助。
每个异步方法都开始以同步方式执行。当它遇到一个 await 时,它(默认情况下)捕获当前上下文,并稍后使用该上下文恢复执行该方法。因此,当它从 UI 线程调用时,async 方法会在捕获的 UI 上下文上恢复。控制台应用程序不提供上下文,因此 async 方法会在线程池线程上恢复。
“询问任务是否在单独的线程上运行”的首选说法是什么?这样我就可以在必要时建立锁。
我建议采用一种不需要问这种线程问题的设计。首先,您可以使用普通的锁 - 在没有争用时,它们非常快速:
async Task BackgroundWorkAsync() // Called from any thread
{
  while (moreToProcess)
  {
    var item = await GetItemAsync();
    lock (_mutex)
        Items.Add(item);
  }
}

或者,您可以记录组件依赖于提供的一次性上下文,并在控制台应用程序中使用类似 我的 AsyncEx 库中的 AsyncContext 的东西。


谢谢,是的,显然至少对于Windows Forms应用程序来说,似乎我可以忽略所有同步机制,因为更新将在UI线程上运行。但正如接受答案的评论中所述,调用Control.Invoke来更新列表和UI是否更好呢?我知道这并不是必要的,但我觉得这会使代码更加“健壮”。也许在将来,有人可能会将此异步任务转换为在线程池(Task.Run)上运行,或者在将其移植到控制台应用程序时,由于我们没有Control.Invoke,编译器将使我们意识到这个问题。 - sapito
@sapito:不要使用Control.Invoke,我建议每个人都这样做。一个简单的lock应该就足够了。 - Stephen Cleary
我理解你对控制台应用程序的观点。但是在Windows Forms上,要么不需要锁定(实际上不需要),要么需要使用Control.Invoke,因为在更新列表后,我还需要更新UI。也就是说,在Windows Forms应用程序中,使用锁定但不使用Control.Invoke将是不一致的。但是我看到你的建议是在WinForms应用程序中根本不使用任何东西。 - sapito
@sapito:我建议您根据当前的使用情况进行设计。如果将来使用情况发生变化,再更改代码即可。 - Stephen Cleary
是的,谢谢。我觉得我陷入了设计瘫痪中。我已经有了几个应该可行的解决方案。现在是时候去实施它了... - sapito

2
Asyc-await会在await语句之前捕获同步上下文,然后默认情况下在await语句之后在同一上下文中运行继续执行。UI线程的同步上下文仅与一个线程相关联,因此在这种情况下,您可能始终会从UI线程更新列表。

但是,如果有人更改代码以在其中一个等待后调用ConfigureAwait(false),则继续执行将不会在原始同步上下文中运行,您可能会在其中一个线程池线程上更新列表。

另请注意,您不能在lock语句内使用await,但可以使用SemaphoreSlim进行异步等待。

  1. 在我看来,最好只使用同步的集合而不是依赖于从同一线程更新列表。

  2. 您不能假设当前同步上下文将被捕获,但继续执行可能并不总是在其上运行。

  3. 在这种情况下,我将使用同步集合或SempahoreSlim。对于控制台应用程序,将使用线程池同步上下文,继续执行可能会在任何一个线程池线程上运行。

  4. 对于IO绑定代码,使用async-await是有意义的,因为它不会消耗线程。

我建议继续使用async-await并更改为使用线程安全的集合或使用SemaphoreSlim进行同步。


好的,谢谢你提出的建议。我会考虑在控制台应用程序中使用它们。但现在我的主要关注点是 Windows 表单应用程序。 - sapito

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