非线程安全函数异步安全吗?

问题描述 投票:0回答:4

考虑以下修改非线程安全列表的异步函数:

async Task AddNewToList(List<Item> list)
{
    // Suppose load takes a few seconds
    Item item = await LoadNextItem();
    list.Add(item);
}

简单地说:这安全吗?

我担心的是,人们可能会调用异步方法,然后在加载该方法时(无论是在另一个线程上,还是作为 I/O 操作),调用者可能会修改列表。

例如,假设调用者正在执行 list.Clear() ,突然 Load 方法完成!会发生什么?

任务会立即中断并运行

list.Add(item);
代码吗?或者它会等到主线程完成所有计划的 CPU 任务(即:等待 Clear() 完成),然后再运行代码? 编辑:因为我基本上已经在下面为自己回答了这个问题,所以这里有一个额外的问题:为什么?为什么它立即中断而不是等待 CPU 密集型操作完成?不排队似乎是违反直觉的,这将是完全安全的。

编辑:这是我自己测试的另一个示例。注释表示执行顺序。我很失望!

TaskCompletionSource<bool> source;
private async void buttonPrime_click(object sender, EventArgs e)
{
    source = new TaskCompletionSource<bool>();  // 1
    await source.Task;                          // 2
    source = null;                              // 4
}

private void buttonEnd_click(object sender, EventArgs e)
{
    source.SetResult(true);                     // 3
    MessageBox.Show(source.ToString());         // 5 and exception is thrown
}
c# asynchronous thread-safety
4个回答
4
投票

简短回答

使用异步时始终需要小心。

更长的答案

这取决于您的 SynchronizationContextTaskScheduler,以及您所说的“安全”是什么意思。

当您的代码

awaits
某些内容时,它会创建一个延续并将其包装在一个任务中,然后将其发布到当前 SynchronizationContext 的 TaskScheduler。然后,上下文将确定延续运行的时间和地点。默认调度程序只是使用线程池,但不同类型的应用程序可以扩展调度程序并提供更复杂的同步逻辑。

如果您正在编写没有 SynchronizationContext 的应用程序(例如,控制台应用程序或ASP.NET core 中的任何内容),则延续将简单地放在线程池中,并且可以与主线程并行执行。在这种情况下,您必须使用

lock
或同步对象,例如
ConcurrentDictionary<>
而不是
Dictionary<>
,用于除本地引用或与任务一起关闭 的引用以外的任何内容。

如果您正在编写 WinForms 应用程序,则延续将被放入消息队列中,并将全部在主线程上执行。这使得使用非同步对象变得安全。然而,还有其他担忧,例如“死锁”。当然,如果您生成任何线程,则必须确保它们使用 lock 或并发对象,并且

任何 UI 调用都必须编组回 UI 线程
。另外,如果您足够疯狂地编写一个具有多个消息泵的 WinForms 应用程序(这是非常不寻常的),您就需要担心同步任何公共变量。 如果您正在编写 ASP.NET 应用程序,SynchronizationContext 将确保对于给定的请求,不会有两个线程同时执行。您的延续可能会在不同的线程上运行(由于称为“线程敏捷性”的性能功能),但它们将始终具有相同的 SynchronizationContext,并且可以保证没有两个线程会同时访问您的变量(假设,当然,它们不是静态的,在这种情况下它们跨越 HTTP 请求并且必须同步)。此外,管道将阻止同一会话的并行请求,以便它们串行执行,因此您的会话状态也不会受到线程问题的影响。然而你仍然需要担心死锁。

当然,您可以编写自己的 SynchronizationContext 并将其分配给您的线程,这意味着您可以指定将与

async

一起使用的自己的同步规则。 另请参阅

yield 和await 如何在.NET 中实现控制流?

不,这不安全。然而,还要考虑到调用者也可能在调用代码之前生成一个线程并将 List 传递给其子线程,即使在非异步环境中也是如此,这也会产生相同的有害影响。


2
投票

假设“无效访问”发生在

LoadNextItem()

0
投票
Task

将抛出异常。由于上下文被捕获,它将传递到调用者线程,因此将无法到达

list.Add

所以,不,它不是线程安全的。
    

是的,我认为这可能是一个问题。


0
投票

private async void GetIntButton(object sender, RoutedEventArgs e) { List<int> Ints = new List<int>(); Ints.Add(await GetInt()); } private async Task<int> GetInt() { await Task.Delay(100); return 1; }

但是你必须调用并异步,所以我不认为这也能工作。

	

© www.soinside.com 2019 - 2024. All rights reserved.