2014-08-30 36 views
0

我已经将.NET 4.0 WinForms程序升级到.NET 4.5.1,希望在异步WCF调用中使用新的await以防止在等待数据的同时冻结用户界面(原始文件很快写入,所以我希望旧的同步WCF调用可以通过使用新的await功能对现有代码进行最小限度的更改来实现异步)。等待异步WCF调用不返回到UI线程和/或阻止UI线程

从我个人理解,应该等待对该返回到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 
    } 
} 

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

现在该程序实际上是一个Hydra VisualPlugin,在一个旧的Delphi应用程序中运行,所以如果有什么可能会搞砸的话,可能会发生......但是没有任何人有任何经验,究竟是什么可以等待异步WCF不返回到UI线程或挂起UI线程? 也许从4.0升级到4.5.1会让程序错过一些有关魔术的参考吗?

现在,虽然我想了解为什么await不能像广告一样工作,但我最终做出了自己的解决方法:自定义awaiter强制任务在后台线程中运行,并强制延续返回在UI线程上。 类似于.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确实需要使用异步和在代码中等待,或者如果多层任务可能会出现问题(我基本上绕过了封装的Task自己的返回方法 - 因为它没有在我的WCF服务程序中正确返回)。

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

回答

2

现在这个程序实际上是一个水润VisualPlugin一个老德尔福申请

这可能是问题的内部运行。正如我在我的async intro blog post中解释的那样,当您awaitTask并且该任务不完整时,默认情况下,await运算符将捕获“当前上下文”,并稍后恢复该上下文中的async方法。 “当前上下文”是SynchronizationContext.Current,除非它是null,在这种情况下它是TaskScheduler.Current

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

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

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

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

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

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

+0

不幸的是'SynchronizationContext.Current'在等待之前和之后都不为空,但是 - 同步上下文或任务调度程序可能不标准,因为.NET UI线程与Delphi程序共享消息处理,这会导致await逻辑以某种方式失败。 很高兴知道这是一个普通的插件问题,而不仅仅是我错过了WCF设置中的一些魔力 - 谢谢! – 2014-09-01 07:56:19

+0

如果'SynchronizationContext.Current'是'await'前的'WinFormsSynchronizationContext',那么它*应该*在UI线程上恢复... – 2014-09-01 12:45:52