ожидание асинхронного вызова WCF, не возвращающегося в поток пользовательского интерфейса и/или блокирующего поток пользовательского интерфейса

Я обновил программу .NET 4.0 WinForms до .NET 4.5.1 в надежде использовать новый await для асинхронных вызовов WCF, чтобы предотвратить зависание пользовательского интерфейса при ожидании данных (оригинал был быстро написан, поэтому я надеялся, что старый синхронные вызовы WCF можно сделать асинхронными с минимальными изменениями в существующем коде с помощью новой функции ожидания).

Насколько я понимаю, ожидание должно было вернуться в поток пользовательского интерфейса без дополнительного кодирования, но по какой-то причине это не для меня, поэтому следующее приведет к исключению перекрестного потока:

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, чтобы вернуться к потоку пользовательского интерфейса, который разрешил исключение, но затем я обнаружил, что мой пользовательский интерфейс все еще завис, несмотря на использование асинхронно задач, созданных Visual Studio 2013 для моей службы WCF.

Теперь программа на самом деле представляет собой Hydra VisualPlugin, работающий внутри старого приложения Delphi, поэтому, если что-то может испортить ситуацию, вероятно, произойдет... Но есть ли у кого-нибудь опыт в том, что именно может сделать ожидание асинхронного WCF, не возвращающегося в поток пользовательского интерфейса или повесить UI-нить? Может быть, обновление с 4.0 до 4.5.1 приводит к тому, что программа пропускает какую-то ссылку, чтобы творить чудеса?

Теперь, когда я хотел бы понять, почему ожидание не работает так, как рекламируется, в итоге я придумал свой собственный обходной путь: настраиваемый ожидатель, который заставляет задачу выполняться в фоновом потоке и заставляет продолжение вернуться в поток пользовательского интерфейса. . Аналогично .ConfigureAwait(false) я написал расширение .RunWithReturnToUIThread(this) для Taks следующим образом:

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 нужно использовать асинхронность и ожидание в своем коде, или если несколько уровней задач могут создавать проблемы (я в основном обхожу инкапсулировал собственный метод возврата задачи, поскольку он не возвращался правильно в моей программе для служб WCF).

Любые комментарии/идеи относительно эффективности вышеуказанного обходного пути или того, с чего начались проблемы?


person Allan K.    schedule 30.08.2014    source источник


Ответы (1)


Теперь программа представляет собой Hydra VisualPlugin, работающий внутри старого приложения Delphi.

Это, вероятно, проблема. Как я объясняю в своем async вводном сообщении в блоге, когда вы await Task и эта задача не завершена, оператор await по умолчанию зафиксирует «текущий контекст», а затем возобновит выполнение метода async в этом контексте. «Текущий контекст» равен SynchronizationContext.Current, если только он не null, в этом случае он равен TaskScheduler.Current.

Таким образом, нормальное поведение «возврата в поток пользовательского интерфейса» является результатом await захвата контекста синхронизации пользовательского интерфейса — в случае WinForms WinFormsSynchronizationContext.

В обычном приложении WinForms SynchronizationContext.Current устанавливается в WinFormsSynchronizationContext при первом создании Control. К сожалению, это не всегда происходит в архитектурах плагинов (я видел подобное поведение в плагинах Microsoft Office). Я подозреваю, что когда ваш код ожидает, SynchronizationContext.Current будет null, а TaskScheduler.Current будет TaskScheduler.Default (т. е. планировщик задач пула потоков).

Итак, первое, что я бы попробовал, это создать Control:

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

Надеюсь, вам придется сделать это только один раз, при первом вызове вашего плагина. Но вам, возможно, придется сделать это в начале всех ваших методов, которые могут быть вызваны хостом.

Если это не сработает, вы можете создать свой собственный SynchronizationContext, но лучше всего использовать WinForms, если можете. Пользовательский ожидающий также возможен (и если вы пойдете по этому пути, легче обернуть TaskAwaiter<T>, а не Task<T>), но недостаток пользовательского ожидающего заключается в том, что он должен выполняться каждые await.

person Stephen Cleary    schedule 30.08.2014
comment
К сожалению, SynchronizationContext.Current не является нулевым ни до, ни после ожидания, но да - контекст синхронизации или планировщик задач, вероятно, нестандартны, потому что поток пользовательского интерфейса .NET совместно обрабатывает сообщения с программой Delphi, что приводит к сбою логики ожидания. Приятно знать, что это обычная проблема с плагином, а не только то, что мне не хватает магии в настройке WCF - спасибо! - person Allan K.; 01.09.2014
comment
Если SynchronizationContext.Current является WinFormsSynchronizationContext перед await, то он должен возобновляться в потоке пользовательского интерфейса... - person Stephen Cleary; 01.09.2014