【问题标题】:Can I ignore WCF asynchronous EndXXX call?我可以忽略 WCF 异步 EndXXX 调用吗?
【发布时间】:2014-04-29 04:47:01
【问题描述】:

我有 2 个服务于 WCF 调用。我从客户端向两个服务发送相同的异步 WCF BeginXXX 调用,然后开始等待带有 WaitHandle.WaitAny(waitHandles) 的回复,其中 waitHandles 是来自 2 个 BeginXXX 调用返回的 IAsyncResults 的 WaitHandles 数组。

我只想使用响应速度更快的服务的回复,即当 WaitHandle.WaitAny 返回索引时,我只使用相应的 IAsyncResult 调用 EndXXX 以获得更快的结果。我从不打电话给另一个 EndXXX。

我这样做的原因是,有时服务在垃圾收集中使用了几秒钟,并且无法快速响应。根据我的经验,这两个服务通常在不同的时间进行垃圾收集,所以其中一个几乎总是能够返回一个快速的答案。我的客户端应用程序对时间非常关键,我需要在几毫秒内得到答案。

我的问题是:

  1. 我可以安全地忽略为响应速度较慢的其他服务调用 EndXXX 方法吗?我对较慢的结果不感兴趣,但想尽快使用较快的结果。根据我的实验,即使我不为相应的较慢 BeginXXX 异步结果调用 EndXXX 方法,似乎也不会发生任何不好的事情。

  2. 如果我没有为相应的 BeginXXX 发出 EndXXX 调用,有人介意向我解释一下究竟会发生什么吗?在 Visual Studio 中的调试器下,我似乎能够看到另一个答案是通过 I/O 完成端口在 .NET 框架中处理的,并且该处理并非源自调用 EndXXX 的客户端。由于没有进行 EndXXX 调用,我似乎没有任何内存泄漏。我认为所有涉及的对象都被垃圾回收了。

  3. 服务器端方法 XXX 实现是单个同步 XXX 还是显式异步 BeginXXX/EndXXX 对有什么区别吗?

  4. 恕我直言,同步 XXX 方法实现将始终返回一个答案 需要在某个地方处理。它发生在客户端还是服务器上 当我没有打电话给 EndXXX 时,我的情况是怎样的?

  5. 使用 WaitHandles 是一种等待最快结果的好方法吗?

  6. 如果我必须为我发送的每个 BeginXXX 调用 EndXXX,事情就会变得很尴尬。我将不得不将无趣的 EndXXX 调用委托给另一个线程,该线程只会忽略结果。在我的原始线程中调用所有 EndXXX 调用会破坏以同步方式获取和使用更快答案的目的。

【问题讨论】:

  • 您所针对的框架版本是什么?也许您可以使用基于异步Task 的模式,它可以处理EndXXX: stackoverflow.com/a/21498956/1768303
  • AFAIK 基于任务的模式使用线程池来处理其工作。我的客户端每秒会进行数千次调用,并且会完全破坏线程池。
  • 我在answer@aku 中解决了您的问题。

标签: c# multithreading performance wcf asynchronous


【解决方案1】:
  1. 文档说您必须调用 end 方法。如果您违反了文档的要求,那么您将处于未定义的行为领域。资源可能会泄漏。也许他们只是在负载下才这样做,谁知道呢?
  2. 我不知道,抱歉。我给出部分答案。我的建议:实现一个什么都不做的服务方法,并在一个循环中调用它 10M 次。资源会泄露吗?如果是,您就有答案了。
  3. 不,服务器和客户端是独立的。服务器可以同步,客户端异步,反之亦然。两者甚至都无法区分对方所做的事情。这两个服务由 TCP 和一个定义良好的协议分开。客户端甚至不可能知道服务器在做什么。服务器甚至不必使用 .NET。
  4. 我不确定你在问什么。在后台,WCF 客户端使用 TCP。传入的数据将在“某处”处理(实际上是在线程池上)。
  5. 如果您的代码基本上是同步的,那么这是您能做的最好的事情。您将烧毁一个线程等待 N 个异步服务调用。没关系。
  6. 为什么不在BeginXXX 中指定一个除了调用EndXXX 之外什么都不做的回调? 这样你总是调用EndXXX 并符合框架的使用方式。您仍然可以使用等待句柄。

【讨论】:

  • 您的第 6 点是一个绝妙的主意!为什么我自己没有想到呢? ;o) 我让回调执行所有 EndXXX 调用,但使用 ManualResetEvent 向原始线程发出第一个信号。大概我不能同时使用异步结果中的回调和句柄。我不知道哪个是第一个被呼叫/发出信号的?
  • 你不在乎哪个先发信号。但是,如果您确实关心它,您可以手动创建一个事件并在回调中发出信号。异步 IO 通常不与 evens 一起使用,因为重点是 等待。您的情况是一个例外,因为您只为多个异步 IO 等待一次。
  • 在我的例子中,上面的客户端逻辑是同步的,所以想法是将同步调用转换为对多个服务器的异步调用,并使用最快的结果。感谢您的想法,我有第 6 点,回调启动并运行良好。也简单易懂。
  • @aku 太棒了。您可以使用 Task.Factory.FromAsync 将 APM 模式转换为 TAP 模式。然后你可以说Task.WaitAny 来获得最快的任务。这会自动处理 EndXXX 的内容,并使代码尽可能干净。
【解决方案2】:

取决于您调用开始/结束模式的对象。有些已知会泄漏。 Jeffrey Richter 通过 C# 来自 CLR:

你必须调用 Endxxx 否则你会泄漏资源。 CLR 分配一些 启动异步操作时的内部资源。如果结束xxx 永远不会被调用,这些资源只有在 进程终止。

【讨论】:

  • 我通过 C# 获得 CLR,第 4 版为 PDF。我在里面找不到你报价的任何部分。你能指定你的来源,即版本和页面吗?
  • 在我看来,Richter's CLR via C#,第 4 版已经用新的 Task async/await 内容替换了关于原始异步模式的章节。
  • 抱歉,没有注意到第一条评论。它在第 2 版第 615 页(关于 APM 的重要说明部分)。也许第三个也是,我不知道。
【解决方案3】:

AFAIK 基于任务的模式使用线程池来处理其工作。 我的客户每秒拨打数千个电话,并且会完全 丢弃线程池。

如果您使用Task.RunTask.Factory.StartNew,就会如此。 Task.Factory.FromAsync 本身不会显式创建或切换线程。

回到你的场景:

我只想使用回复速度更快的服务的回复, 即当 WaitHandle.WaitAny 返回索引时,我只调用 EndXXX 使用相应的 IAsyncResult 以获得更快的结果。我不 永远不要打电话给另一个 EndXXX。

让我们为BeginXXX/EndXXX 异步服务调用创建Task 包装器:

public static class WcfExt
{
    public static Task<object> WorkAsync(this IService service, object arg)
    {
        return Task.Factory.FromAsync(
             (asyncCallback, asyncState) =>
                 service.BeginWork(arg, asyncCallback, asyncState),
             (asyncResult) =>
                 service.EndWork(asyncResult), null);
    }
}

并实现 whatever-service-answers-faster 逻辑:

static async Task<object> CallBothServicesAsync(
    IService service1, IService service2, object arg)
{
    var task1 = service1.WorkAsync(arg);
    var task2 = service2.WorkAsync(arg);

    var resultTask = await Task.WhenAny(task1, task2).ConfigureAwait(false);
    return resultTask.Result;
}

到目前为止,还没有阻塞代码,我们仍然没有显式地创建新线程。 WorkAsync 包装器将继续回调传递给 BeginWork。当BeginWork启动的操作完成后,服务会调用这个回调。

它会在whatever线程上被调用来完成这样的操作。大多数情况下,这是来自线程池的随机 IOCP(输入/输出完成端口)线程。有关更多详细信息,请查看 Stephen Cleary 的"There Is No Thread"。完成回调会自动调用EndWork完成操作并取回结果,因此服务不会泄露资源,并将结果存储在Task&lt;object&gt;实例中(由WorkAsync返回)。

然后,await Task.WhenAny 之后的代码将继续在该特定线程上执行。所以,await之后可能会有线程切换,但自然是使用了异步操作已经完成的IOCP线程。

您几乎不需要使用低级同步原语,例如手动重置事件和任务并行库。例如,如果您需要等待CallBothServicesAsync 的结果,您可以这样做:

var result = CallBothServicesAsync(service1, service2).Result;
Console.WriteLine("Result: " + result);

等同于:

var task = CallBothServicesAsync(service1, service2);
task.Wait();
Console.WriteLine("Result: " + task.result);

此代码将阻塞当前线程,类似于 WaitHandle.WaitAny 在您的原始场景中所做的。

现在,也不推荐这样的阻塞,因为您会失去异步编程模型的优势并损害应用程序的可扩展性。被阻塞的线程可能正在做一些其他有用的工作,而不是等待,例如,对于 Web 应用程序,它可能正在为另一个传入的客户端请求提供服务。

理想情况下,您的逻辑应该是“一直异步”,直到某个根入口点。例如,使用控制台应用程序:

static async Task CoreLoopAsync(CancellationToken token)
{
    using(var service1 = CreateWcfClientProxy())
    using(var service2 = CreateWcfClientProxy())
    {
        while (true)
        {
            token.ThrowIfCancellationRequested();
            var result = await CallBothServicesAsync("data");
            Console.WriteLine("Result: " + result);
        }
    }
}

static void Main()
{
    var cts = CancellationTokenSource(10000); // cancel in 10s
    try
    {
        // block at the "root" level, i.e. inside Main
        CoreLoopAsync(cts.Token).Wait();
    }
    catch (Exception ex)
    {
        while (ex is AggregatedException)
            ex = ex.InnerException;
        // report the error
        Console.WriteLine(ex.Message);
    }
}

【讨论】:

  • 非常有趣的想法。我已经有 usr 的想法 6(异步回调调用 EndXXX)工作,代码简单易懂,但有时间我会试试你的。它看起来非常优雅,但可能不像 usr 的想法那么容易理解 6。我在分布式异步调用之上的客户端逻辑是同步的,所以我需要等待以获得最快的结果。我同意客户端可能会一路异步执行得更好,但我没有选择重写它。
  • 想法是将同步调用转换为异步调用到多个服务器并使用最快的结果。
  • @aku,对。您将避免阻塞等待,您的服务将更好地扩展。另外,async/await 将大大简化您的代码结构,包括异常处理。
  • 我对有时会从 EndXXX 调用中得到的通信异常有进一步的担忧,因为通道处于故障状态。在那种情况下,框架是否会因为我没有通过 EndXXX 调用而清理资源?在这种情况下我应该做些什么吗?
  • 关于 EndXXX 呼叫中的故障通道还有另一个问题:我可以通过 BeginXXX 呼叫之外的其他通道进行 EndXXX 呼叫吗?我的客户对每项服务都开放了多个渠道。
猜你喜欢
  • 1970-01-01
  • 2011-11-27
  • 2016-10-05
  • 1970-01-01
  • 1970-01-01
  • 2016-08-23
  • 2014-06-14
  • 2011-04-24
  • 1970-01-01
相关资源
最近更新 更多