【问题标题】:Calling Task-based methods from ASMX从 ASMX 调用基于任务的方法
【发布时间】:2014-07-27 12:49:35
【问题描述】:

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

我最近一直在将一个包含旧版 ASMX Web 服务的 ASP.NET 2.0 项目更新到 ASP.NET 4.5。作为更新的一部分,我引入了一个 Web API 接口,以实现应用程序的高级自动化。 ASMX 服务必须与新 API 共存以实现向后兼容。

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

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

这引起了很大的麻烦:ASMX 服务如何与新的异步数据访问功能愉快地共存?

【问题讨论】:

    标签: c# asp.net-web-api task-parallel-library asmx


    【解决方案1】:

    我最近一直在将一个包含旧版 ASMX Web 服务的 ASP.NET 2.0 项目更新到 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 方法。在这种情况下,该“上下文”是一个 ASP.NET 请求上下文,它一次只允许一个线程。因此,当 asmx 代码在任务上进一步阻塞(通过WaitAll)时,它会阻塞该请求上下文中的线程,并且async 方法无法完成。

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


    但更好的改进是“一直”使用异步调用。 (旁注:我在MSDN article on async best practices 中对此进行了更详细的描述)。

    ASMX does allow 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);
    }
    

    【讨论】:

    • 里面有一些非常有用的信息——谢谢!我宁愿不对 Web 服务接口本身进行任何实质性更改(即将其转换为使用 APM),但使用 Task.Run 无疑是避免死锁的更好方法。我也尽可能地使用了 ConfigureAwait(false),尽管野兽的本质是我不能保证它会一直被使用,因为理论上某些数据源驱动程序可以由第 3 方编写未来。
    • 如果您使用Task.Run,那么您将失去所有异步代码的好处。当然,这是您的决定,但请注意,除非您将 asmx api 设为异步 (APM),否则它们不会获得异步代码的任何好处(例如可扩展性)。
    • @SomBhattacharyya:是的。 AFAIK,asmx 是一项死技术,因此尚未更新为直接使用Task(可能永远不会)。所以你仍然必须使用 APM 风格的异步方法。另请注意,异步可能不会给您“更好的性能” - 这取决于您的意思。异步会为您提供更好的可扩展性(扩展得更远更快),但不会为每个请求提供更好的响应时间
    • @SomBhattacharyya:如果您需要可扩展性,请在 asmx 中使用 APM。否则,像 GetAwaiter().GetResult() 这样的阻塞调用是调用异步代码的唯一选择。
    • @SomBhattacharyya:不;无论您如何阻止,您仍然会面临死锁风险。
    【解决方案2】:

    经过进一步调查,我发现初始任务创建的子任务可以毫无问题地等待,但从未检测到父任务(即 ASMX 中返回 Task 的方法调用)已完成.

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

    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 调用成功完成,但是为每个异步调用生成一个新线程显然有点暴力;欢迎提供替代方案、改进或建议!

    【讨论】:

    • 你有没有发现让这个更“好”? :D
    【解决方案3】:

    这可能是一个老话题,但它包含了我能找到的最佳答案,以帮助维护使用 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();
    

    【讨论】:

      猜你喜欢
      • 2013-04-04
      • 2016-01-31
      • 1970-01-01
      • 2021-06-07
      • 2013-08-24
      • 1970-01-01
      • 2020-06-28
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多