我最近正在升级一个包含遗留 ASMX Web 服务的 ASP.NET 2.0 项目到 ASP.NET 4.5。首先要做的是确保在你的 web.config 文件中
httpRuntime@targetFramework
设置为 4.5
。
父任务(即在 ASMX 中返回 Task 的方法调用)从未被检测为已完成。
这实际上是一个经典的死锁情况。我在我的博客上
详细描述了它, 但其要点是,
await
(默认情况下)会捕获一个“上下文”并使用它来恢复
async
方法。在这种情况下,“上下文”是ASP.NET请求上下文,该上下文一次只允许一个线程。因此,当asmx代码在堆栈中更高处通过
WaitAll
阻塞任务时,它会阻塞请求上下文中的一个线程,而
async
方法无法完成。
将阻塞等待推送到后台线程可能“可行”,但正如您所指出的那样,这有点蛮力。一个小改进是只使用
var result = Task.Run(() => MethodAsync()).Result;
,将后台工作排队到线程池中,然后阻塞请求线程等待其完成。或者,您可以选择在每个
await
上使用
ConfigureAwait(false)
,它会覆盖默认的“上下文”行为,并允许
async
方法在请求上下文之外的线程池线程上继续执行。
但更好的改进是使用异步调用。 (附注:我在MSDN关于async
最佳实践的文章中详细描述了这一点)。
ASMX 允许 使用APM类型的异步实现。我建议您首先尽可能将asmx实现代码异步化(即使用await WhenAll
而不是WaitAll
)。您最终会得到一个“核心”方法,然后需要在APM API中进行包装。
包装器应该类似于:
private Task<string> FooAsync(int arg);
[WebMethod]
public IAsyncResult BeginFoo(int arg, AsyncCallback callback, object state)
{
var tcs = new TaskCompletionSource<string>(state);
var task = FooAsync(arg);
task.ContinueWith(t =>
{
if (t.IsFaulted)
tcs.TrySetException(t.Exception.InnerExceptions);
else if (t.IsCanceled)
tcs.TrySetCanceled();
else
tcs.TrySetResult(t.Result);
if (callback != null)
callback(tcs.Task);
});
return tcs.Task;
}
[WebMethod]
public string EndFoo(IAsyncResult result)
{
return ((Task<string>)result).GetAwaiter().GetResult();
}
这可能会变得有些乏味,如果你需要包装很多方法,因此我在我的
AsyncEx库中编写了一些
ToBegin
和ToEnd
方法。使用这些方法(或者你自己的副本,如果你不想依赖库),包装器就会简化得很好:
[WebMethod]
public IAsyncResult BeginFoo(int arg, AsyncCallback callback, object state)
{
return AsyncFactory<string>.ToBegin(FooAsync(arg), callback, state);
}
[WebMethod]
public string EndFoo(IAsyncResult result)
{
return AsyncFactory<string>.ToEnd(result);
}
Task
(并且也可能永远不会被更新)。因此,您仍然必须使用 APM 风格的异步方法。 还要注意的是 async 可能不会给您带来“更好的性能”-这取决于您的具体意思。异步将为您提供更好的 可扩展性(扩大规模和更快),但不会为每个请求提供更好的 响应时间。 - Stephen ClearyGetAwaiter().GetResult()
这样的阻塞调用是调用异步代码的唯一选择。 - Stephen Cleary