从ASMX调用基于任务的方法

时间:2014-06-06 09:35:02

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

我最近有一段经验值得分享,这可能对任何必须维护必须更新的旧版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服务如何能够与新的异步数据访问功能共存?

3 个答案:

答案 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();