异步调用后的跨线程异常

时间:2017-06-07 17:02:24

标签: c# database multithreading async-await synchronizationcontext

下面的代码块仅使用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中的代码并找到了这个链接: https://github.com/npgsql/npgsql/blob/2dd46e7c544caf3302ca7b89dd888a16dccf5c2c/src/Npgsql/PGUtil.cs

在文件的底部,它说:

  

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

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

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

1 个答案:

答案 0 :(得分:4)

  

npgsql临时禁用SynchronizationContext的方式是最佳做法吗?

没有。 的想法并不错:对于内部方法,null SynchronizationContext.Current SynchronizationContext.Current。但是,their implementation有问题,因为它确实清除了来电者SynchronizationContext

这是因为原始await必须同步恢复,而不是NoSynchronizationContextScope.Disposable之后。在表面异步方法向其调用者返回一个不完整的任务之前,必须先将public async Task<NpgsqlLargeObjectStream> OpenReadAsync(uint oid, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); using (NoSynchronizationContextScope.Enter()) return await OpenRead(oid, true); } 处理

因此,使用this simple example

OpenReadAsync

操作顺序为:

  • 某些线程调用cancellationToken
  • 检查NoSynchronizationContextScope.Enter
  • SynchronizationContext.Current保存并清除OpenRead
  • await被调用并返回不完整的任务。
  • 任务为OpenReadAsync,导致SynchronizationContext返回其来电者。
  • 调用主题已丢失其OpenRead

稍后,当OpenReadAsync返回的任务完成时:

  • 拾取线程池线程以继续执行NoSynchronizationContextScope.Disposable
  • 处置SynchronizationContext.Current,将OpenReadAsync设置为原始值。
  • SynchronizationContext返回的任务已完成。
  • 线程池线程的错误public Task<NpgsqlLargeObjectStream> OpenReadAsync(uint oid, CancellationToken cancellationToken) => SynchronizationContextSwitcher.NoContext(async () => { cancellationToken.ThrowIfCancellationRequested(); return await OpenRead(oid, true); });

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

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

{{1}}