2014-08-30 5 views
0

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

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

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 
    } 
} 

После статьи await anything я сделал пользовательский awaiter, чтобы я мог опубликовать await this, чтобы вернуться к потоку пользовательского интерфейса, который решил исключение, но затем я обнаружил, что мой пользовательский интерфейс все еще заморожен, несмотря на использование асинхронных задач, созданных Visual Studio 2013 для моего WCF оказание услуг.

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

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

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

ответ

2

Теперь программа фактически Hydra VisualPlugin работает внутри старого приложения Delphi

Это, вероятно, проблема. Как я объясняю в своем async intro blog post, когда вы awaitTask, и эта задача неполна, оператор 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, если сможете. Также возможен пользовательский awaiter (и если вы идете по этому маршруту, проще обернуть TaskAwaiter<T>, а не Task<T>), но недостатком пользовательского awaiter является то, что он должен идти каждый await.

+0

К сожалению, «SynchronizationContext.Current» не является нулевым ни до, ни после ожидания, но да - контекст синхронизации или планировщик задач, вероятно, нестандартен, потому что поток .NET UI разделяет обработку сообщений с помощью программы Delphi, что вызывает логику ожидания как-то не получается. Приятно знать, что это нормальная проблема с плагином, а не только отсутствие какой-либо магии в настройке WCF - спасибо! –

+0

Если 'SynchronizationContext.Current' является' WinFormsSynchronizationContext' перед 'await', то он должен * должен * возобновляться в потоке пользовательского интерфейса ... –

Смежные вопросы