我最近有一段经验值得分享,这可能对任何必须维护必须更新的旧版ASMX Web服务以调用基于任务的方法的人有所帮助。
我最近一直在更新ASP.NET 2.0项目,其中包括一个传统的ASMX Web服务到ASP.NET 4.5。作为更新的一部分,我介绍了一个Web API接口,以允许应用程序的高级自动化。 ASMX服务必须与新API共存才能实现向后兼容。
该应用程序的一个功能是能够代表调用者从外部数据源(工厂历史数据库,定制Web服务等)请求数据。作为升级的一部分,我重新编写了数据访问层的大部分内容,以使用基于任务的异步模式异步请求数据。鉴于在ASMX服务中不可能使用aync / await,我修改了ASMX方法以对异步方法进行阻塞调用,即调用基于任务的方法,然后使用Task.WaitAll阻塞线程,直到任务完成。
调用任何正在调用返回任务或任务的方法的ASMX方法< T>在引擎盖下,我发现请求总是超时。当我逐步完成代码时,我可以看到异步代码已成功执行,但对Task.WaitAll的调用从未检测到任务已完成。
这引起了一个令人头疼的问题:ASMX服务如何能够与新的异步数据访问功能共存?
答案 0 :(得分:14)
我最近一直在更新一个ASP.NET 2.0项目,其中包含一个传统的ASMX Web服务到ASP.NET 4.5。
首先要做的是确保httpRuntime@targetFramework
is set to 4.5
in your web.config
。
父任务(即返回任务的ASMX中的方法调用)从未被检测为完成。
这实际上是一种典型的死锁情况。我describe it in full on my blog,但它的要点是await
将(默认情况下)捕获"上下文"并使用它来恢复async
方法。在这种情况下,那个" context"是一个ASP.NET请求上下文,一次只允许一个线程。因此,当asmx代码进一步上升任务上的堆栈阻止时(通过WaitAll
),它阻塞了该请求上下文中的一个线程,并且async
方法无法完成
将阻塞等待推送到后台线程会#34;工作",但是你注意到它有点蛮力。一个小的改进就是使用var result = Task.Run(() => MethodAsync()).Result;
,它将后台工作排队到线程池,然后阻止请求线程等待它完成。或者,您可以选择为每ConfigureAwait(false)
使用await
,这会覆盖默认的"上下文"行为并允许async
方法继续在线程池线程外部请求上下文。
但更好的改进是使用异步调用"一路"。 (旁注:我在MSDN article on async
best practices中更详细地描述了这一点。)
does allow的ASMX APM variety异步实现。我建议您首先使asmx实现代码尽可能异步(即使用await WhenAll
而不是WaitAll
)。你最终会得到一个"核心"然后您需要wrap in an APM API的方法。
包装器看起来像这样:
// Core async method containing all logic.
private Task<string> FooAsync(int arg);
// Original (synchronous) method looked like this:
// [WebMethod]
// public string Foo(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();
}
如果你有很多方法要包装,这会有点乏味,所以我写了一些ToBegin
and ToEnd
methods作为AsyncEx library的一部分。使用这些方法(或者如果你不想要库依赖项,你自己的副本),包装器可以很好地简化:
[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);
}
答案 1 :(得分:5)
经过进一步调查,我发现可以等待初始任务创建的子任务没有任何问题,但是父任务(即ASMX中返回任务&lt; T&gt;的方法调用)从未被检测为完成。
调查使我认为遗留Web服务堆栈和任务并行库之间存在某种不兼容性。我想出的解决方案涉及创建一个新线程来运行基于任务的方法调用,这个想法是一个单独的线程不会受到处理ASMX请求的线程中存在的线程/任务管理不兼容性的影响。为此,我创建了一个简单的辅助类,它将运行一个Func&lt; T&gt;在新线程中,阻塞当前线程,直到新线程终止,然后返回函数调用的结果:
public class ThreadRunner<T> {
// The function result
private T result;
//The function to run.
private readonly Func<T> function;
// Sync lock.
private readonly object _lock = new object();
// Creates a new ThreadRunner<T>.
public ThreadRunner(Func<T> function) {
if (function == null) {
throw new ArgumentException("Function cannot be null.", "function");
}
this.function = function;
}
// Runs the ThreadRunner<T>'s function on a new thread and returns the result.
public T Run() {
lock (_lock) {
var thread = new Thread(() => {
result = function();
});
thread.Start();
thread.Join();
return result;
}
}
}
// Example:
//
// Task<string> MyTaskBasedMethod() { ... }
//
// ...
//
// var tr = new ThreadRunner<string>(() => MyTaskBasedMethod().Result);
// return tr.Run();
以这种方式运行基于任务的方法可以很好地工作并允许ASMX调用成功完成,但显然有点暴力为每个异步调用生成一个新线程;欢迎提供替代方案,改进或建议!
答案 2 :(得分:1)
这可能是一个老话题,但它包含了我能找到的最佳答案,以帮助维护遗留代码,使用 ASMX 和 WebMethod 同步调用较新的异步函数。
我是为 stackoverflow 做贡献的新手,所以我没有对 Graham Watts 解决方案发表评论的声誉。我真的不应该回应另一个答案 - 但我还有什么其他选择。
事实证明,格雷厄姆的回答对我来说是一个很好的解决方案。我有一个在内部使用的遗留应用程序。它的一部分称为外部 API,此后已被替换。为了使用替换,旧应用升级到 .NET 4.7,因为替换广泛使用 Tasks。我知道“正确”的做法是重写遗留代码,但没有时间或预算进行如此广泛的练习。
我必须做的唯一改进是捕获异常。这可能不是最优雅的解决方案,但它对我有用。
public class ThreadRunner<T>
{
// Based on the answer by graham-watts to :
// https://stackoverflow.com/questions/24078621/calling-task-based-methods-from-asmx/24082534#24082534
// The function result
private T result;
//The function to run.
private readonly Func<T> function;
// Sync lock.
private readonly object _lock = new object();
// Creates a new ThreadRunner<T>.
public ThreadRunner(Func<T> function)
{
if (function == null)
{
throw new ArgumentException("Function cannot be null.", "function");
}
this.function = function;
}
Exception TheException = null;
// Runs the ThreadRunner<T>'s function on a new thread and returns the result.
public T Run()
{
lock (_lock)
{
var thread = new Thread(() => {
try
{
result = function();
}catch(Exception ex)
{
TheException = ex;
}
});
thread.Start();
thread.Join();
if (TheException != null)
throw TheException;
return result;
}
}
}
// Example:
//
// Task<string> MyTaskBasedMethod() { ... }
//
// ...
//
// var tr = new ThreadRunner<string>(() => MyTaskBasedMethod().Result);
// return tr.Run();