【问题标题】:Are non-thread-safe functions async safe?非线程安全函数异步安全吗?
【发布时间】:2018-11-16 16:28:30
【问题描述】:

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

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 绑定操作完成?不排队似乎是违反直觉的,这将是完全安全的。

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

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
}

【问题讨论】:

  • 答案取决于您的SynchronizationContext,对于不同的应用程序不同。例如,您在 WinForms 和 ASP.NET 中可能是安全的,但在 Console 应用程序或 .NET Core 应用程序中并不安全,因为它们不需要连续运行来连续运行。请编辑您的问题并标记平台。此外,如果您使用async void,所有赌注都将取消。
  • @JohnWu 我用我想出的一个例子来编辑我的问题来为自己测试。这在 WinForms 中绝对不安全。
  • 这实际上更多地展示了 TaskCompletionSource 的一个陷阱。问题是,默认情况下,SetX 方法(SetResult、SetException 等)会导致延续同步运行,而这里source = null 是延续。您必须特别注意如何使用 TaskCompletionSource。
  • @mikez 你能举个例子说明我不会遇到这个问题吗?我错误地认为无论任务如何它都是一致的,但你的解释是有道理的。
  • 只要您的任务/线程以不安全的方式处理共享状态,您就会遇到问题。就这么简单,但这也应该为您提供有关您需要做什么的线索,只是不要使用共享状态,或者至少当您这样做时,请确保您知道该共享状态发生了什么并防止出现问题。

标签: c# asynchronous thread-safety


【解决方案1】:

不,它不安全。然而,还要考虑到调用者在调用你的代码之前也可能已经产生了一个线程并将 List 传递给它的子线程,即使在非异步环境中,这也会产生同样的不利影响。

所以;虽然不安全,但无论如何从调用者那里接收 List 并没有本质上是线程安全的 - 无法知道该列表是否实际上是从您自己的其他线程处理的。

【讨论】:

    【解决方案2】:

    简答

    你总是需要小心使用异步。

    更长的答案

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

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

    如果您正在编写一个没有 SynchronizationContext 的应用程序(例如,一个控制台应用程序,或anything in .NET core),则延续只是放在线程池中,并且可以与您的主线程并行执行。在这种情况下,您必须使用 lock 或同步对象(例如 ConcurrentDictionary&lt;&gt; 而不是 Dictionary&lt;&gt;)来处理本地引用或 closed 与任务的引用以外的任何内容。

    如果您正在编写 WinForms 应用程序,则延续将被放入消息队列中,并将全部在主线程上执行。这使得使用非同步对象变得安全。但是,还有其他担忧,例如deadlocks。当然,如果您产生任何线程,您必须确保它们使用lock 或并发对象,以及any UI invocations must be marshaled back to the UI thread。此外,如果您足够疯狂地编写带有多个消息泵的 WinForms 应用程序(这是非常不寻常的),您需要担心同步任何公共变量。

    如果您正在编写 ASP.NET 应用程序,SynchronizationContext 将确保对于给定的请求,没有两个线程同时执行。您的延续可能在不同的线程上运行(由于称为 thread agility 的性能特性),但它们将始终具有相同的 SynchronizationContext 并且您可以保证没有两个线程会同时访问您的变量(当然,假设,它们不是静态的,在这种情况下它们跨越 HTTP 请求并且必须同步)。此外,管道将阻止对同一会话的并行请求,以便它们串行执行,因此您的会话状态也受到保护,不受线程问题的影响。但是,您仍然需要担心死锁。

    当然,您可以将write your own SynchronizationContext 分配给您的线程,这意味着您可以指定将与async 一起使用的自己的同步规则。

    另见How do yield and await implement flow of control in .NET?

    【讨论】:

    • 由于 .NET Core GUI 应用程序中仍有上下文,不应将“或 .NET Core 中的任何内容”更改为“或 ASP.NET核心应用程序?”
    【解决方案3】:

    假设LoadNextItem() 中出现“无效访问”:Task 将抛出异常。由于上下文被捕获,它将传递给调用者线程,因此不会到达list.Add

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

    【讨论】:

      【解决方案4】:

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

      我会返回项目并添加到主踏板上的列表中。

      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;
      }
      

      但是你必须调用 from 和 async 所以我不这样做这也行。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-05-06
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多