异步调用后跨线程异常

HNG*_*NGO 1 c# database multithreading synchronizationcontext async-await

下面的代码块仅对 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";
          }
       }
    }
}
Run Code Online (Sandbox Code Playgroud)

我查看了 Npgsql 的代码并找到了此链接: https://github.com/npgsql/npgsql/blob/2dd46e7c544caf3302ca7b89dd888a16dccf5c2c/src/Npgsql/PGUtil.cs

文件底部写道:

该机制用于在执行 Npgsql 代码时临时将当前同步上下文设置为 null,从而使所有等待延续在线程池上执行。这取代了在任何地方放置ConfigureAwait(false)的需要,并且应该在所有表面异步方法中使用,无一例外。

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

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

Ste*_*ary 5

npgsql 临时禁用 SynchronizationContext 的方式是否被认为是最佳实践?

不。这个想法不错:采用null内部SynchronizationContext.Current方法。然而,它们的实现是有缺陷的,因为它确实清除了调用者的SynchronizationContext.Current.

这是因为原始数据SynchronizationContext必须同步恢复,而不是在await. 必须在表面异步方法将未完成的任务返回给其调用者之前NoSynchronizationContextScope.Disposable进行处理。

因此,使用这个简单的例子

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

操作顺序是:

  • 一些线程调用OpenReadAsync.
  • cancellationToken检查。
  • NoSynchronizationContextScope.Enter保存然后清除SynchronizationContext.Current
  • OpenRead被调用并返回一个未完成的任务。
  • 该任务被await编辑,这会导致OpenReadAsync返回到其调用者。
  • 调用线程已丢失其SynchronizationContext.

稍后,当任务完成返回时OpenRead

  • 线程池线程被拾取以恢复执行OpenReadAsync
  • NoSynchronizationContextScope.Disposable处置,设置SynchronizationContext.Current为其原始值。
  • 返回的任务OpenReadAsync已完成。
  • 线程池线程现在有一个不正确的SynchronizationContext.

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

这就是为什么SynchronizationContextSwitcher.NoContext强制您传递委托:这样它就可以强制同步进行处理。它的用法比较尴尬,但它必须具有正确的语义:

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