从ASMX调用基于任务的方法

我有一个最近的经验,我想分享这可能对任何必须维护必须更新以调用基于任务的方法的旧ASMX Web服务的人有所帮助。

我最近一直在更新一个ASP.NET 2.0项目,其中包括一个传统的ASMX Web服务到ASP.NET 4.5。 作为更新的一部分,我介绍了一个Web API接口,以允许应用程序的高级自动化。 ASMX服务必须与新API共存才能实现向后兼容。

该应用程序的一个function是能够代表呼叫者从外部数据源(工业工厂历史记录,定制Web服务等)请求数据。 作为升级的一部分,我重新编写了数据访问层的大部分内容,以使用基于任务的异步模式异步请求数据。 鉴于在ASMX服务中不可能使用aync / await,我修改了ASMX方法以对异步方法进行阻塞调用,即调用基于任务的方法,然后使用Task.WaitAll阻塞线程,直到任务完成。

当调用任何正在调用返回任务或任务的方法的ASMX方法时,我发现请求总是超时。 当我逐步完成代码时,我可以看到异步代码已成功执行,但对Task.WaitAll的调用从未检测到任务已完成。

这引起了一个令人头疼的问题:ASMX服务如何能够与新的异步数据访问function共存?

我最近一直在更新一个ASP.NET 2.0项目,其中包括一个传统的ASMX Web服务到ASP.NET 4.5。

首先要确保在web.config中将httpRuntime@targetFramework设置为4.5

从未检测到父任务(即返回任务的ASMX中的方法调用)。

这实际上是一种典型的死锁情况。 我在我的博客上完整地描述了它 ,但它的要点是await将(默认情况下)捕获“上下文”并使用它来恢复async方法。 在这种情况下,“context”是一个ASP.NET请求上下文,它一次只允许一个线程。 因此,当asmx代码进一步向上阻塞任务(通过WaitAll )时,它会阻塞该请求上下文中的一个线程,并且async方法无法完成。

将阻塞等待推送到后台线程将“正常”,但正如您所说,它有点暴力。 一个小的改进就是使用var result = Task.Run(() => MethodAsync()).Result; ,将后台工作排队到线程池,然后阻止请求线程等待它完成。 或者,您可以选择对每个await使用ConfigureAwait(false) ,这会覆盖默认的“context”行为,并允许async方法在请求上下文之外的线程池线程上继续。


但是更好的改进是“一路”使用异步调用。 (旁注:我在关于async最佳实践的MSDN文章中更详细地描述了这一点)。

ASMX 确实允许 APM种类的异步实现。 我建议您首先使asmx实现代码尽可能异步(即使用await WhenAll而不是WaitAll )。 您将最终得到一个“核心”方法,然后您需要将其包装在APM API中 。

包装器看起来像这样:

 // Core async method containing all logic. private Task 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(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)result).GetAwaiter().GetResult(); } 

如果你有很多方法要包装,这会有点繁琐,所以我写了一些ToBeginToEnd方法作为我的AsyncEx库的一部分。 使用这些方法(或者如果你不想要库依赖项,你自己的副本),包装器可以很好地简化:

 [WebMethod] public IAsyncResult BeginFoo(int arg, AsyncCallback callback, object state) { return AsyncFactory.ToBegin(FooAsync(arg), callback, state); } [WebMethod] public string EndFoo(IAsyncResult result) { return AsyncFactory.ToEnd(result); } 

经过进一步调查,我发现可以等待初始任务创建的子任务没有任何问题,但是父任务(即返回Task 的ASMX中的方法调用)从未被检测为完成。

调查使我认为遗留Web服务堆栈和任务并行库之间存在某种不兼容性。 我想出的解决方案涉及创建一个新线程来运行基于任务的方法调用,这个想法是一个单独的线程不会受到处理ASMX请求的线程中存在的线程/任务管理不兼容性的影响。 为此,我创建了一个简单的帮助器类,它将在新线程中运行Func ,阻塞当前线程,直到新线程终止,然后返回函数调用的结果:

 public class ThreadRunner { // The function result private T result; //The function to run. private readonly Func function; // Sync lock. private readonly object _lock = new object(); // Creates a new ThreadRunner. public ThreadRunner(Func function) { if (function == null) { throw new ArgumentException("Function cannot be null.", "function"); } this.function = function; } // Runs the ThreadRunner'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 MyTaskBasedMethod() { ... } // // ... // // var tr = new ThreadRunner(() => MyTaskBasedMethod().Result); // return tr.Run(); 

以这种方式运行基于任务的方法可以很好地工作并允许ASMX调用成功完成,但显然有点暴力为每个异步调用生成一个新线程; 欢迎提供替代方案,改进或建议!