【问题标题】:Async Task.Run in a self hosted web service - is it "wrong usage" as long as it works?Async Task.Run 在自托管的 Web 服务中运行 - 只要它有效,它就是“错误使用”吗?
【发布时间】:2018-10-20 06:06:27
【问题描述】:

我正在做一个包含以下详细信息的项目:

  • 不涉及 IIS 或其他第 3 方网络服务器
  • .NET 4.5 Windows 应用程序,充当“服务器”
  • 服务器启动多个 WCF Web 服务,它们都是自托管的
  • 网络服务可供多个用户访问

以下是众多 Web 服务方法之一的最简单示例:

public async Task<int> Count()
{
        int result = 0;

        //There is nothing to await here, so I use Task.Run
        await Task.Run(() =>
        {
            using (IDB ctx = new DB())
            {
                result = ctx.Customers.All.Count();
            }
            //Here could happen a lot more
           //System.Threading.Thread.Sleep(10000); 

        }).ConfigureAwait(false);

        return result;
}

如您所见,我正在使用 Task.Run 访问一些数据,因为没有一个存储库接口提供异步方法。我不能等待任何事情。如果我想做“真正的异步”,我将不得不重写完整的存储库接口。

如果我不使用 Task.Run,​​服务器将阻止所有其他传入请求。

我的 2 个问题是:

  1. 在这种情况下使用 Task.Run 有什么问题吗?

  2. 即使它工作正常并且可能没有完全错误,是否有更好、更专业的解决方案来在异步方法中调用同步代码?

我读到这个问题的最初原因是,在异步方法中使用 Task.Run 是“假异步”。 (我认为 Task.Run 会启动一个新线程,而 "real async" 代码不会)

我回答了我自己的问题,请参阅下面的答案。我希望它可以帮助其他人。

【问题讨论】:

  • so public async Task&lt;int&gt; Count()? 是 WCF 公开的 OperationContract 吗?
  • This 是一篇很好的文章,解释了为什么这不是一个好主意。显然它可以像你展示的那样工作。缺点是它确实无法利用异步增加的可扩展性,因为它仍然大量使用线程,这在服务器场景中可能有很高的需求。但是,您确实提到了服务器阻止其他请求,这涉及到异步的第二个用例,即卸载工作。在这种情况下,这样做会很有用。
  • @TheGeneral - 是的,这是自托管 Web 服务公开的功能。
  • @mike z - 这就是我使用 Task.Run 的原因。如果存在长时间运行的同步方法,则所有传入流量都会被阻止。
  • 如果您无法更改界面,您可以在方法中添加 await Task.Delay(1) 而不是使用 Task.Run。但值得一提的是,所有使该方法看起来异步的解决方案并不专业。

标签: c# web-services asynchronous server task


【解决方案1】:

是的,它是假的 async,并且由于它启动一个线程并阻塞它,所以它的可伸缩性较低,在它完成之前不会将其归还。

然而,

我称这些方法为“伪异步方法”,因为它们看起来 异步的,但实际上只是通过同步工作来伪装它 在后台线程上。一般情况下,不要在 方法的实施;相反,使用 Task.Run 调用 方法。该指南有两个原因:

  • 您的代码的使用者假定如果一个方法具有异步签名,那么它将真正异步地执行。假装 通过仅在后台线程上执行同步工作来实现异步 是令人惊讶的行为。
  • 如果您的代码曾经在 ASP.NET 上使用过,那么虚假异步方法会导致开发人员走错路。服务器上异步的目标 一方面是可扩展性,而伪异步方法的可扩展性较差 而不仅仅是使用同步方法。

公开“异步优于同步”包装器的想法也是一个非常 滑坡,采取极端可能导致每 在同步和异步中公开的单个方法 形式。许多向我询问这种做法的人是 考虑为长时间运行的 CPU 密集型暴露异步包装器 操作。意图是好的:帮助响应。 但正如前面所说,响应性可以很容易地通过 API的消费者,消费者实际上可以在右边这样做 粗略程度,而不是针对每个健谈的单独操作。 此外,定义哪些操作可以长期运行是 出奇的困难。许多方法的时间复杂度往往 变化很大。

但是,您实际上并不属于这两个类别中的任何一个。根据您的描述,您正在托管此 WCF 服务。如果您正确设置了InstanceContextModeConcurrencyMode,它无论如何都会异步运行您的代码。假设您使用适当的设置生成了代理,您还可以从客户端为您的调用运行 TBA 包装器。

如果我理解正确,你可以让这个方法完全同步,让 WCF 处理细节并节省资源

更新

一个例子:如果我在任何 web 服务方法中使用 Task.Run,​​我可以 甚至在 Task.Run 中调用 Thread.Sleep(10000) 并且服务器保持不变 响应任何传入流量。

我认为以下内容可能对您最有帮助

Sessions, Instancing, and Concurrency

会话是两个端点之间发送的所有消息的关联。 实例化是指控制用户自定义服务的生命周期 对象及其相关的 InstanceContext 对象。并发是 用于控制在一个线程中执行的线程数的术语 InstanceContext 同时进行。

您的 WCF 服务似乎是为 InstanceContextMode.PerSessionConcurrencyMode.Single 设置的。如果你的服务是无状态的,你可能想使用InstanceContextMode.PerCall,并且只有在你有真正可以等待的东西时才使用async

【讨论】:

  • 这是我在这个问题之前读过的文章。一个例子:如果我在任何 web 服务方法中使用 Task.Run,​​我什至可以在 Task.Run 中调用 Thread.Sleep(10000) 并且服务器保持对任何传入流量的响应。
  • 首先感谢您的帮助。我将进一步调查我的问题,看看什么是“最专业”的解决方案。正如我已经说过的,真正的问题是让所有自托管服务对传入流量做出响应。这需要几天时间才能深入了解。
  • 如果你还有兴趣,看看我的回答。这是另一个问题,但你的提示帮助我解决了这个问题。
【解决方案2】:

首先:感谢大家的提示。我需要他们深入研究这个问题。

我已经找到了解决这个问题的真正方法,我认为,我可以通过详细回答我自己的问题来为社区增加一些价值。

也可以在这篇精彩的文章中找到解决方案:https://www.oreilly.com/library/view/learning-wcf/9780596101626/ch04s04.html

下面是对最初问题和解决方案的简要总结:

目标

  • 我的目标是在 .NET 4.5 应用程序中托管多个自托管 WCF 服务
  • 所有自托管 WCF 服务都可供多个客户端访问
  • 当多个用户使用它们时,所有自托管 WCF 服务不得相互阻止

问题(和错误的解决方案)(我最初的问题)

  • 我的问题是,每当一个客户端使用 Web 服务时,它都会阻止其他 Web 服务,直到它返回给客户端
  • 我使用哪种 InstanceContextMode 或 ConcurrencyMode 并不重要
  • 我的错误解决方案是使用 async 和 Task.Run(“假异步”)。它有效,但不是真正的解决方案。

解决方案(见文章)

  • 自托管 WCF Web 服务时,您必须确保始终在单独的线程中调用 ServiceHost.Open,与 UI 线程不同
  • 当您在控制台、WinForms 或 WPF 应用程序或 Windows 服务中打开 ServiceHost 时,您必须了解何时调用 ServiceHost.Open 以及如何使用 ServiceBehaviorAttribute.UseSynchronizationContext
  • ServiceBehaviorAttribute.UseSynchronizationContext 的默认值为 True。 (这很糟糕,会导致阻塞!)
  • 如果只是调用ServiceHost.Open,没有设置UseSynchronizationContext = false,所有的ServiceHost都会运行在UI线程中,并相互阻塞这个线程。

解决方案 1(经过测试并且有效 - 不再阻塞)

  • 设置 ServiceBehaviorAttribute.UseSynchronizationContext = false

解决方案 2(经过测试并且有效 - 不再阻塞)

  • 不要触碰 ServiceBehaviorAttribute.UseSynchronizationContext,让它成为真的
  • 但至少创建一个或多个调用 ServiceHost.Open 的线程

代码:

private List<ServiceHost> _ServiceHosts = new List<ServiceHost>();
private List<Thread> _Threads = new List<Thread>(); 

foreach (ServiceHost host in _ServiceHosts)
{
   _Threads.Add(new Thread(() => { host.Open(); }));
   _Threads[_Threads.Count - 1].IsBackground = true;
   _Threads[_Threads.Count - 1].Start();
}

解决方案 3(未测试,但在文章中提及)

  • 不要碰 ServiceBehaviorAttribute.UseSynchronizationContext,让它成为真的
  • 但请确保在创建 UI 线程之前调用 ServiceHost.Open
  • 那么 ServiceHosts 将使用不同的线程并且不会阻塞 UI 线程

我希望这可以帮助其他有同样问题的人。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-02-23
    • 1970-01-01
    • 1970-01-01
    • 2017-02-06
    • 1970-01-01
    • 1970-01-01
    • 2013-11-30
    • 1970-01-01
    相关资源
    最近更新 更多