异步任务和锁

Async tasks and locks

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

由于第二个进程是 I/O 绑定的,它似乎适合异步任务。这引出了几个问题:

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

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

  3. 我说的是 Windows Forms 应用程序。也许将来我希望它作为控制台应用程序运行。 AFAIK,在控制台应用程序中,异步任务在单独的线程上运行。如果任务在单独的线程上运行,询问任务的首选习惯用法是什么?这样我就可以在必要的时候建立一个锁

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

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

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

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

  1. 恕我直言,最好只使用 synchronized collection 而不是依赖从同一线程更新的列表。

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

  3. 在这种情况下我会使用同步集合或 SempahoreSlim。对于控制台应用程序,使用线程池同步上下文,并且延续可以在任何线程池线程上结束 运行ning。

  4. 对 IO 绑定代码使用异步等待是有意义的,因为它不消耗线程。

我会坚持使用 async-await 并改为使用线程安全集合或使用 SemaphoreSlim

进行同步

Since async tasks don't run on separate threads, it seems I don't need any kind of lock when updating this list, right?

不能保证不使用 Task.Run 并标记他的方法 async。 IO 绑定异步任务很可能 不在幕后使用某种线程,but that isn't always the case. 您不应该依赖它来确保代码的正确性。您可以通过使用另一个不使用 ConfigureAwait(false)async 方法来确保您的代码 运行 在 UI 线程上运行。您始终可以使用框架中提供的并发集合。也许 ConcurrentBagBlockingCollection 可以满足您的需要。ui

AFAIK, in console applications Async tasks run on separate threads. What's the preferred idiom to ask a task if it's running on a separate thread?

这是不正确的。 async 他们自己的操作不会 运行 在单独的线程上,因为它们在控制台应用程序中。简而言之,控制台应用程序中的默认值 TaskScheduler 是默认值 ThreadPoolTaskScheduler,它将在线程池线程上对任何延续进行排队,因为控制台没有这样的称为 ui 线程的实体。一般来说,it's all about SynchronizationContext

The fact that I don't know if I really need a lock makes me wonder wether this is the best design or not. Would it make sense to stick to Task.Run() even for this kind of IO bound code?

绝对不是。您不知道的事实是您发布此问题的原因,也是我们试图提供帮助的原因。

不需要使用线程池线程来执行异步 IO。 IO 中异步的全部意义在于,您可以释放执行 IO 的调用线程以处理更多工作,同时处理请求。

Since async tasks don't run on separate threads, it seems I don't need any kind of lock when updating this list, right?

没错。如果您遵循功能模式(即每个后台操作将 return 结果,而不是更新共享数据),则此方法效果很好。所以,像这样的东西可以正常工作:

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

在这种情况下,GetItemAsync 的实现方式并不重要。它可以随心所欲地使用 Task.RunConfigureAwait(false) - BackgroundWorkAsync 将在将项目添加到集合之前始终与 UI 线程同步。

Maybe in the future I want it to run it as a console application. AFAIK, in console applications Async tasks run on separate threads.

"Async tasks" 根本不要 运行。如果这令人困惑,我有一个 async intro 可能会有所帮助。

每个异步方法开始时都是同步执行的。当它遇到 await 时,它(默认情况下)捕获当前上下文并稍后使用它来恢复执行该方法。因此,当从 UI 线程调用它时会发生什么,即 async 方法在捕获的 UI 上下文中恢复。控制台应用程序不提供上下文,因此 async 方法在线程池线程上恢复。

What's the preferred idiom to ask a task if it's running on a separate thread? This way I can establish a lock when necessary.

我推荐一种不会提出此类线程问题的设计。首先,您可以只使用普通的 lock - 在没有争用的情况下,它们 非常 快:

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

或者,您可以记录组件依赖于提供的一次一个上下文,并为控制台应用程序使用类似 AsyncContext from my AsyncEx library 的内容。