我已经将.NET 4.0 WinForms程序升级到.NET 4.5.1,希望在异步WCF调用上使用新的await,以防止在等待数据时冻结UI(原文很快写入,所以我是希望旧的同步WCF调用可以使用新的await功能对现有代码进行最小的更改而异步。
根据我的理解,await应该返回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之后我做了一个自定义等待工具,所以我可以发出一个await this
来回到UI线程,这解决了异常,但后来我发现我的UI仍然被冻结,尽管使用Visual Studio 2013为我的WCF服务生成的异步任务。
现在程序实际上是一个在旧的Delphi应用程序中运行的Hydra VisualPlugin,所以如果有什么东西可能搞砸了,可能会发生......但是有没有人有任何关于等待异步WCF无法返回的经验UI线程还是挂起UI线程? 也许从4.0升级到4.5.1会使程序错过一些参考来做魔术吗?
现在,虽然我想了解为什么await不像宣传的那样工作,但我最终制定了自己的解决方法:一个自定义等待器,强制任务在后台线程中运行,并强制继续返回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真的需要在其代码中使用async和await,或者如果多层Tasks会产生问题(我是基本上绕过了封装的Task自己的返回方法 - 因为它在我的WCF服务程序中没有正确返回。)
关于上述变通方法的效率的任何意见/想法,或者导致问题开始的原因是什么?
答案 0 :(得分:2)
现在程序实际上是在旧的Delphi应用程序中运行的Hydra VisualPlugin
这可能就是问题所在。正如我在async
intro blog post中解释的那样,当您await
Task
并且该任务不完整时,await
运算符默认会捕获“当前上下文”并稍后恢复该上下文中的async
方法。 “当前上下文”为SynchronizationContext.Current
,除非它是null
,在这种情况下它是TaskScheduler.Current
。
因此,正常的“返回UI线程”行为是await
捕获UI同步上下文的结果 - 在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的缺点是必须每隔{{1 }}