异步网络服务器和垃圾收集

Asynchronous webserver and garbage collection

在阅读了 Stephen Cleary 关于异步的 article 和 asp.net 之后,很明显在 Web 应用程序中使用异步在可伸缩性方面是一个巨大的胜利(主要是由于免费的非阻塞线程来满足更多请求)。

虽然我对异步操作返回的任务对象如何保留在 Aysnc WebServer 实现的范围内感到有点困惑(假设是 iis 或自托管 WebApi) 而不会被收集。

例如,

如果我们在低级别 Web 服务器实现中有以下方法,

// some method that handles HttpListeners BeginContextAsync callback, 
public void SomeHttpListenerCallback(HttpListenerContext context)
{

    // Immediately set up the next begin context            

    // then handle the request
    // forgive me if this is incorrect but this is my understanding of how an async request would be handled
    // please feel free to point out the flaws
    var task  = messageHanlder1.SendAsync(context.Request, null); 
    task.ContinueWith(t => SendResponse(t));

}

public class MessageHandler1 : DelegatingHandler
{
    protected async override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        Debug.WriteLine("Process request");
        // Call the inner handler.
        var response = await base.SendAsync(request, cancellationToken);
        Debug.WriteLine("Process response");
        return response;
    }
}

SendResponse 是将响应发送到客户端套接字的某种方法。

您可能已经注意到,任务对象在 SomeHttpListenerCallback 中超出了范围,因为没有其他线程(理论上)对任务对象有任何引用,它不会被标记为在下一个 GC 周期中收集吗?

我已经阅读了 Jeffrey Ritcher 关于编译器如何将 aysnc 方法转换为带有状态机的方法的解释(通过 C# 的 Clr),但是我无法理解如果没有线程等待任务对象会发生什么。

如果一个线程等待它,我们不会为每个请求阻塞 1 个线程吗? 任何帮助我指明正确方向的帮助将不胜感激。

对于您执行的每个异步操作,其背后都有一些 "native" 操作 - 套接字读取或文件 IO 等。这些是本机 CLR 操作或 PInvoke 调用。当开始这样的操作时,框架会注册一个回调以在完成时调用。此回调用于完成任务。该回调由 "the system" 保持活动状态。例如,当您说 myFileStream.BeginRead(myCallback) 时,回调 将被调用 ,即使它未被引用。

简而言之,本机操作本身使完成时必须处理的所有内容保持活动状态。

这不是硬性规定,但如果 IO 框架只是有时调用您的回调,那么它就毫无用处。

没有什么可以阻止 any Task 被收集。例如var dummy = new TaskCompletionSource<object>().Task;立即符合领取条件。

首先,如果您有一个 Task 并且没有任何引用它,那么它将被 GC 收集。

然而,几乎从来没有这种情况。有 2 种类型的任务,Promise 任务和 Delegate 任务。

Promise 任务(异步)主要由 TaskCompletionSourceasync 方法的编译器创建。在这两种情况下,有人持有对任务的引用,因此它能够完成它。

然而,委托任务(同步)由在其中执行委托的线程引用。

在你的例子中,taskSendAsync 后面的状态机引用,直到它完成,而 task 正在引用延续 Task。当它完成时,延续被调度,因此被 TaskScheduler 和执行的线程 SendResponse 引用。操作完成后,任务将不再被引用,最终可能会 GCed.


您可以在 implementation of Task.Delay 中看到一个示例,其中 Task(实际上 DelayPromise 继承自 Task)被 System.Threading.Timer 引用用于完成 Task.

因为有些参数实际上不需要计时器,所以您可以看到这两者在内存使用方面的区别:

static void Main()
{
    while (true)
    {
        Task.Delay(int.MaxValue);
    }
}

还有这个:

static void Main()
{
    while (true)
    {
        Task.Delay(-1); // No Timer as the delay is infinite.
    }
}