从ASMX调用基于任务的方法

16

我有一个最近的经验想分享给大家,可能对于任何需要维护遗留 ASMX web 服务并必须更新为调用基于任务的方法的人有所帮助。

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

应用程序的一个功能是能够代表调用者从外部数据源(工业设备历史记录、定制 Web 服务等)请求数据。作为升级的一部分,我重新编写了数据访问层的重要部分,使用了基于任务的异步模式异步请求数据。由于无法在 ASMX 服务中使用 aync/await,请注意我修改了 ASMX 方法,使其阻塞调用异步方法,例如调用基于 Task 的方法,然后使用 Task.WaitAll 阻塞线程直到任务完成。

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

这导致了一个很大的问题:ASMX 服务如何能够与新的异步数据访问功能和谐共存呢?

3个回答

23
我最近正在升级一个包含遗留 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中进行包装

包装器应该类似于:

// 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();
}

这可能会变得有些乏味,如果你需要包装很多方法,因此我在我的AsyncEx库中编写了一些 ToBeginToEnd方法。使用这些方法(或者你自己的副本,如果你不想依赖库),包装器就会简化得很好:
[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);
}

@StephenCleary,您现在在2018年仍然推荐同样的建议吗?我正在开发类似的项目,使用asmx,需要更好的性能。我看到代码库中到处都是.Result和.Wait之类的代码行,感觉不太对劲。需要一些建议。 - Som Bhattacharyya
2
@SomBhattacharyya:是的。据我所知,asmx 是一项已死技术,因此它没有被更新以直接使用 Task (并且也可能永远不会被更新)。因此,您仍然必须使用 APM 风格的异步方法。 还要注意的是 async 可能不会给您带来“更好的性能”-这取决于您的具体意思。异步将为您提供更好的 可扩展性(扩大规模和更快),但不会为每个请求提供更好的 响应时间 - Stephen Cleary
2
@SomBhattacharyya:如果你需要可伸缩性,那么请在asmx中使用APM。否则,像GetAwaiter().GetResult()这样的阻塞调用是调用异步代码的唯一选择。 - Stephen Cleary
2
@SomBhattacharyya:不管你如何阻塞,你仍然会面临死锁风险。 - Stephen Cleary
好的。感谢帮助。这将有助于为我们的函数找到一个新方向(技术债)。 - Som Bhattacharyya
显示剩余5条评论

5
经过进一步调查,我发现由初始任务创建的子任务可以轻松等待完成,但父任务(即返回Task<T>的ASMX方法调用)从未被检测到完成。
调查让我推测遗留Web服务堆栈和任务并行库之间存在某种不兼容性。我想出的解决方案是创建一个新线程来运行基于任务的方法调用,其想法是单独的线程不会受到在处理ASMX请求的线程中存在的线程/任务管理不兼容性的影响。为此,我创建了一个简单的帮助类,将在新线程中运行Func<T>,阻止当前线程直到新线程终止,然后返回函数调用的结果:
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调用成功完成,但显然为每个异步调用生成一个新线程有些粗暴;欢迎提出替代方案、改进或建议!

1
你找到任何方法让这个更“好看”了吗? :D - Bart Calixto

1

这可能是一个老话题,但它包含了我能够找到的最好的答案,以帮助使用 ASMX 和 WebMethod 调用较新异步函数的遗留代码同步维护。

我是 stackoverflow 的新贡献者,所以我没有声望来发表对 Graham Watts 解决方案的评论。我不应该回复另一个答案,但我还有什么选择呢。

Graham 的答案已经被证明是对我的一个很好的解决方案。我有一个内部使用的遗留应用程序。其中一部分调用了一个已被替换的外部 API。为了使用替代品,遗留应用程序已升级到 .NET 4.7,因为替代品广泛使用 Tasks。我知道“正确”的做法是重写遗留代码,但没有时间或预算进行这样的广泛练习。

我唯一需要做的增强是捕获异常。这可能不是最优雅的解决方案,但对我而言它有效。

 public class ThreadRunner<T>
    {
        // Based on the answer by graham-watts to :
        // https://dev59.com/qmAf5IYBdhLWcg3w_G2r#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();

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接