繁体   English   中英

异步调用后的跨线程异常

[英]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的代码并找到了以下链接: https : //github.com/npgsql/npgsql/blob/2dd46e7c544caf3302ca7b89dd888a16dccf5c2c/src/Npgsql/PGUtil.cs

在文件的底部,它说:

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

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

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

是否将npgsql暂时禁用SynchronizationContext的方式视为最佳实践?

号的想法是不坏的一个:以nullSynchronizationContext.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);
}

操作顺序为:

  • 一些线程调用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);
  });

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM