异步调用后跨线程异常

Cross-thread exception after async call

以下代码块仅在Npgsql(不是sqlclient、sqlite、mysql、文件读取异步)导致跨线程无效操作异常。

private async void button1_Click(object sender, EventArgs e)
{
   var strBuilder = new Npgsql.NpgsqlConnectionStringBuilder()
   {
        Host = "localhost",
        Username = "postgres",
        Password = "password"
   };
   using (var conn = new Npgsql.NpgsqlConnection(strBuilder.ConnectionString))
   {
      try
      {
          await conn.OpenAsync();
          if (conn.State ==ConnectionState.Open)
          {
             MessageBox.Show("Connected");
             this.button1.Text = "CROSS-THREAD-With-NPGSQL";
          }
       }
    }
}

我查看了 Npgsql 的代码,发现了这个 link: https://github.com/npgsql/npgsql/blob/2dd46e7c544caf3302ca7b89dd888a16dccf5c2c/src/Npgsql/PGUtil.cs

在文件的底部,它说:

This mechanism is used to temporarily set the current synchronization context to null while executing Npgsql code, making all await continuations execute on the thread pool. This replaces the need to place ConfigureAwait(false) everywhere, and should be used in all surface async methods, without exception.

我从 Roji(Npgsql 存储库的所有者)那里得到了相当多的解释,但我需要理解为什么我没有看到其他驱动程序的类似问题。 npgsql 临时禁用 SynchronizationContext 的方式是否被认为是最佳实践?我正在尝试查看其他驱动程序的源代码,但这需要一段时间,所以我希望我能得到一些帮助,朝着正确的方向前进。

编辑 1: Stephen Cleary 在下面给出了非常详细的答案,但我也想 post 在这里分享我的一些发现。它可能会帮助其他人。 2016 年 9 月 24 日,npgsql 将所有 ConfigureAwait(false) 替换为 NoSynchronizationContextScope。正如 Stephen 所解释的,NoSynchronizationContextScope 临时清除了调用者上下文,从而导致了此类行为。另一方面,ConfigureAwait(false) 不会做这样的事情,也不应该被替换。为了验证,我安装了npgsql 3.1.7(09/24/16之前的版本),我没有再看到跨线程异常了。

Is the way npgsql temporary disabling the SynchronizationContext considered best practice?

没有。 idea 不错:为了内部方法 null out SynchronizationContext.Current。但是,their implementation 有问题,因为它确实清除了调用者的 SynchronizationContext.Current.

这是因为原始 SynchronizationContext 必须 同步 恢复,而不是在 await 之后。 NoSynchronizationContextScope.Disposable 必须在 之前 表面异步方法 return 对其调用者而言是一项未完成的任务。

因此,使用 this simple example:

public async Task<NpgsqlLargeObjectStream> OpenReadAsync(uint oid, CancellationToken cancellationToken)
{
  cancellationToken.ThrowIfCancellationRequested();
  using (NoSynchronizationContextScope.Enter())
    return await OpenRead(oid, true);
}

运算顺序为:

  • 一些线程调用 OpenReadAsync
  • 检查cancellationToken
  • NoSynchronizationContextScope.Enter 保存然后清除 SynchronizationContext.Current
  • OpenRead 被调用并且 return 是一个未完成的任务。
  • 任务已 awaited,这导致其调用者 OpenReadAsync 到 return。
  • 调用线程已丢失其 SynchronizationContext

稍后,当从 OpenRead 编辑的任务 return 完成时:

  • 一个线程池线程被拾取继续执行OpenReadAsync
  • 释放 NoSynchronizationContextScope.Disposable,将 SynchronizationContext.Current 设置为其原始值。
  • OpenReadAsync 编辑的任务 return 已完成。
  • 线程池线程现在有一个不正确的 SynchronizationContext

所以,不,我会说这完全是错误的。

这就是 my SynchronizationContextSwitcher.NoContext 强制您传递委托的原因:因此它可以强制同步进行处理。它的用法比较笨拙,但它必须具有正确的语义:

public Task<NpgsqlLargeObjectStream> OpenReadAsync(uint oid, CancellationToken cancellationToken) =>
  SynchronizationContextSwitcher.NoContext(async () =>
  {
    cancellationToken.ThrowIfCancellationRequested();
    return await OpenRead(oid, true);
  });