【问题标题】:Does C# AsyncCallback creates a new thread?C# AsyncCallback 是否创建一个新线程?
【发布时间】:2014-01-27 17:45:12
【问题描述】:

我写了一个HttpListener,它监听其中一个端口:

httpListener.BeginGetContext(new AsyncCallback(ListenerCallback), httpListener);

ListenerCallback 处理在侦听器 uri 上收到的任何请求。如果在处理请求期间发生异常,它会运行一个诊断例程,该例程会尝试访问侦听器 uri,以检查侦听器是否实际上是活动的并正在侦听 uri,并写入侦听器返回的响应日志。侦听器只是将字符串 Listening... 返回到此类虚拟请求。

现在在测试过程中,当其他模块发生异常导致执行诊断模块时,我可以看到侦听器在检查日志时正确返回了Listening...。 但是,当ListenerCallback 中发生异常时,尝试在诊断中访问侦听器 URI 时会引发以下异常:

System.Net.WebException : The operation has timed out
   at System.Net.HttpWebRequest.GetResponse()
   at MyPackage.Diagnostics.hitListenerUrl(String url) in c:\SW\MyApp\MyProj\Diagnostics.cs:line 190

诊断模块第190行如下:

189     HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
190     HttpWebResponse response = (HttpWebResponse)request.GetResponse();

现在,如果AsyncCallback 调度新线程并在该新线程中运行ListenerCallback,则当通过诊断发送虚拟请求时,它一定不会导致Operation Timeout。这就是我认为应有的行为,因为它是*Async*Callback。其实MSDN也有says the same

使用 AsyncCallback 委托在单独的线程中处理异步操作的结果。

但似乎并非如此。我在这里错过了什么吗?

视觉解读:

【问题讨论】:

  • “通过诊断发送虚拟请求”完全不清楚。看来您是在问自己的理论,这很可能是错误的。
  • @HenkHolterman 我已将诊断的第 190 行放在了显示我如何向侦听器 uri 发送请求的位置。
  • 所以一个崩溃的监听器停止监听...
  • 是的,不会完全停止侦听而是很忙,因为诊断仅在侦听器线程中运行,如果ListenerCallback 不在新线程上执行,则请求通过@987654337 发送到uri @'s 线程的诊断永远不会得到任何响应。添加了图表,请检查。它记录Operation Timeout 并退出诊断。但是由于ListenerCallback 中的操作失败,另一个依赖线程(有一个循环)抛出异常并在每个循环中运行诊断。对于那些诊断ListenerCallback 正确返回Listening...
  • 你一直提到一些“诊断”:那是什么?这是你的程序中的东西吗?

标签: c# .net multithreading http asynchronous


【解决方案1】:

这完全是类的 BeginXxx() 方法的实现细节。有两种基本方案:

  • BeginXxx() 启动一个线程来完成工作,该线程进行回调
  • BeginXxx() 请求操作系统完成工作,使用 I/O 完成端口请求在完成时得到通知。操作系统启动一个线程来传递通知,该线程运行回调。

第二种方法是非常理想的,它可以很好地扩展程序能够有许多挂起的操作。并且是 HttpListener 使用的方法,Windows 上的 TCP/IP 驱动程序堆栈支持完成端口。您的程序可以轻松支持 数千个 套接字,这在服务器场景中很重要。

回调中的 EndXxx() 调用会报告在尝试通过抛出异常来完成 I/O 请求时遇到的任何事故。在您的情况下, BeginGetContext() 在回调中需要 EndGetContext() 。如果您没有捕捉到异常,那么您的程序将终止。

您的代码 sn-p 实际上并未演示任何异步 I/O。您调用了 GetResponse() 而不是 BeginGetResponse()。根本不涉及回调,因此失败并抛出异常的是 GetResponse() 方法。

【讨论】:

  • 哦,现在我想我明白了。但会欣赏有关如何使用 IO 完成端口处理 HttpListener 回调的更多信息/链接。我想我需要阅读大量内容,可能是 IO 完成端口和 Windows 内核线程工作的一些低级细节。但如果你有一些相关链接,仍然会很感激。 (链接this 答案包含一些相关的材料。)
  • 它们是一个相当高级的概念,真正理解它们需要对设备驱动程序的工作方式有基本的了解。很多关于它的书,Russinovich 的 Windows Internals 非常好。
【解决方案2】:

C# AsyncCallback 是否创建新线程?

不,它不是本身,它只是一个回调。但是,调用回调的代码可能会在与开始原始操作的线程不同的线程上调用它(在您的情况下,操作是httpListener.BeginGetContext)。

通常(但不是必需的)它是在一个随机的ThreadPool 线程上调用的,该线程恰好处理了底层套接字 IO 操作的完成(有关更多详细信息,请参阅:There Is No Thread)。

使用 AsyncCallback 委托来处理 在单独的线程中进行异步操作。

我认为这意味着您应该在调用回调的线程上处理异步操作的结果。该线程几乎总是与启动操作的线程分开,如上所述。这也是他们的sample code 显然所做的。请注意,它们不会在 ProcessDnsInformation 中创建任何新线程。

收到回调后,如何组织服务器应用程序的线程模型由您决定。主要问题是,它应该保持响应性和可扩展性。 理想情况下,您应该在它到达的同一线程上处理传入请求,并在完成处理请求所需的任何 CPU 密集型作业后立即释放该线程。 作为处理逻辑,您可能需要执行其他 IO 绑定任务(访问文件、执行 DB 查询、调用 Web 服务等)。在执行此操作时,您应该尽可能多地使用相关 API 的异步版本,以避免阻塞请求线程(再次参考There Is No Thread)。

IMO,使用您选择的 Asynchronous Programming Model (APM) 模式,实现这样的逻辑可能是一项相当乏味的任务(尤其是其中的错误处理和恢复部分)。

但是,使用基于Task Parallel Library (TPL)async/await patternTask 的API,如HttpListener.GetContextAsync,它是小菜一碟。最好的部分:您不必再担心线程。

为了让您了解我在说什么,这里是an example of a low-level TCP server。在实现基于 HttpListener 的 HTTP 服务器时可以使用非常相似的概念。

【讨论】:

    【解决方案3】:

    为了补充 Hans 的出色回答,

    即使执行异步操作,它实际上也有可能同步完成——即使在方案#2 中,说回调将在另一个线程上返回是不正确的。指望这种行为实际上可能最终导致您陷入死锁或堆栈溢出。

    您可以检查IAsyncResult.CompletedSynchronously 属性来确定这一点。当设置为 true 时,完成回调很可能来自启动异步操作的同一线程:回调将在您的 Begin* 调用期间运行,并且必须完成才能返回。

    【讨论】:

    • 非常好的和重要的一点。在这种情况下,继续处理另一个线程上的操作结果实际上可能是个好主意(使用ThreadPool.QueueUserWorkItemTask.Run)。
    • @Noseratio 是的,类似于this 对吗?
    • @Mahesha999,类似that 的东西确实有效,但它会通过同步的GetContext 调用阻止来自ThreadPool 的线程。我会选择HttpListener.GetContextAsync,它不需要专用线程。您真的不需要掌握内核内部知识就可以利用异步 API。
    猜你喜欢
    • 2016-06-09
    • 1970-01-01
    • 2012-03-13
    • 1970-01-01
    • 1970-01-01
    • 2022-08-18
    • 1970-01-01
    • 1970-01-01
    • 2020-09-27
    相关资源
    最近更新 更多