等待异步WCF调用未返回到UI线程和/或阻塞UI线程

本文关键字:UI 线程 WCF 异步 调用 返回 等待 | 更新日期: 2023-09-27 18:20:07

我已经将.NET 4.0 WinForms程序升级到.NET 4.5.1,希望在异步WCF调用中使用新的wait,以防止在等待数据时冻结UI(原始调用编写得很快,所以我希望使用新的await功能,可以使旧的同步WCF调用异步,对现有代码的更改最小)。

据我所知,wait应该在没有额外编码的情况下返回到UI线程,但由于某种原因,它不适合我,所以下面会给出跨线程异常:

private async void button_Click(object sender, EventArgs e)
{
    using (MyService.MyWCFClient myClient = MyServiceConnectFactory.GetForUser())
    {
        var list=await myClient.GetListAsync();
        dataGrid.DataSource=list; // fails if not on UI thread
    }
}

在这篇文章之后,我做了一个自定义的awaiter,这样我就可以发出await this来返回UI线程,这解决了异常,但后来我发现,尽管使用了Visual Studio 2013为我的WCF服务生成的异步任务,我的UI仍然被冻结。

现在这个程序实际上是一个HydraVisualPlugin,运行在一个旧的Delphi应用程序中,所以如果有什么事情可能会把事情搞砸,那很可能就是。。。但是,有人知道等待异步WCF不返回UI线程或挂起UI线程究竟是什么原因吗?也许从4.0升级到4.5.1会让程序错过一些发挥魔力的参考?

现在,虽然我想了解为什么等待不能像广告中所说的那样工作,但我最终还是制定了自己的解决方案:一个自定义的awaiter,它强制任务在后台线程中运行,并强制继续返回UI线程。类似于.ConfigureAwait(false),我为Taks编写了一个.RunWithReturnToUIThread(this)扩展,如下所示:

public static RunWithReturnToUIThreadAwaiter<T> RunWithReturnToUIThread<T>(this Task<T> task, Control control)
{
  return new RunWithReturnToUIThreadAwaiter<T>(task, control);
}

public class RunWithReturnToUIThreadAwaiter<T> : INotifyCompletion
{
  private readonly Control m_control;
  private readonly Task<T> m_task;
  private T m_result;
  private bool m_hasResult=false;
  private ExceptionDispatchInfo m_ex=null; //  Exception 
  public RunWithReturnToUIThreadAwaiter(Task<T> task, Control control)
  {
    if (task == null) throw new ArgumentNullException("task");
    if (control == null) throw new ArgumentNullException("control");
    m_task = task;
    m_control = control;
  }
  public RunWithReturnToUIThreadAwaiter<T> GetAwaiter() { return this; }
  public bool IsCompleted
  {
    get
    {
      return !m_control.InvokeRequired && m_task.IsCompleted; // never skip the OnCompleted event if invoke is required to get back on UI thread
    }
  }
  public void OnCompleted(Action continuation)
  {
    // note to self: OnCompleted is not an event - it is called to specify WHAT should be continued with ONCE the result is ready, so this would be the place to launch stuff async that ends with doing "continuation":
    Task.Run(async () =>
    {
      try
      {
        m_result = await m_task.ConfigureAwait(false); // await doing the actual work
        m_hasResult = true;
      }
      catch (Exception ex)
      {
        m_ex = ExceptionDispatchInfo.Capture(ex); // remember exception
      }
      finally
      { 
        m_control.BeginInvoke(continuation); // give control back to continue on UI thread even if ended in exception
      }
    });
  }
  public T GetResult()
  {
    if (m_ex == null)
    {
      if (m_hasResult)
        return m_result;
      else
        return m_task.Result; // if IsCompleted returned true then OnCompleted was never run, so get the result here
    }
    else
    {  // if ended in exception, rethrow it
      m_ex.Throw();
      throw m_ex.SourceException; // just to avoid compiler warning - the above does the work
    }
  }
}

现在,在上面的文章中,我不确定是否需要这样的异常处理,或者Task.Run是否真的需要在其代码中使用异步和等待,或者多层Tasks是否会出现问题(我基本上绕过了封装的Task自己的返回方法,因为它在我的WCF服务程序中没有正确返回)。

对上述解决方法的效率有任何意见/想法,或者是什么导致了问题的开始?

等待异步WCF调用未返回到UI线程和/或阻塞UI线程

现在这个程序实际上是一个HydraVisualPlugin,运行在一个旧的Delphi应用程序中

这可能就是问题所在。正如我在async介绍博客文章中所解释的,当您awaitTask,而该任务未完成时,默认情况下await操作符将捕获"当前上下文",然后在该上下文中恢复async方法。"当前上下文"是SynchronizationContext.Current,除非是null,在这种情况下是TaskScheduler.Current

因此,正常的"返回UI线程"行为是await捕获UI同步上下文的结果——在WinForms的情况下,是WinFormsSynchronizationContext

在普通的WinForms应用程序中,第一次创建Control时,SynchronizationContext.Current会设置为WinFormsSynchronizationContext。不幸的是,这种情况在插件体系结构中并不总是发生(我在Microsoft Office插件上看到过类似的行为)。我怀疑当您的代码等待时,SynchronizationContext.CurrentnullTaskScheduler.CurrentTaskScheduler.Default(即线程池任务调度程序)。

所以,我尝试的第一件事就是创建一个Control:

void EnsureProperSynchronizationContext()
{
  if (SynchronizationContext.Current == null)
    var _ = new Control();
}

希望您只需要在第一次调用插件时这样做一次。但是,您可能必须在主机可以调用的所有方法开始时执行此操作。

如果这不起作用,您可以创建自己的SynchronizationContext,但如果可以的话,最好使用WinForms。自定义的awaiter也是可能的(如果你走这条路,包装TaskAwaiter<T>比包装Task<T>更容易),但自定义awaiter的缺点是必须在每个await上进行。