You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

同步方法中使用Task.Delay导致内存占用过高的原因探究

同步方法中未await的Task.Delay引发内存暴涨且GC不自动回收的原因

问题代码

using (var client = new HttpClient() { Timeout = System.TimeSpan.FromSeconds(15)})
{
    var response = client.GetAsync("https://...");

    var canProceed = false;    

    while (!canProceed)
    {
        if (response.IsCompleted)
        {
            canProceed = true;
        }
        Task.Delay(25);
    }; 
}

1. Task.Delay的底层逻辑

Task.Delay并非直接让线程休眠,而是通过线程池定时器实现延迟:每次调用都会创建两个核心对象:

  • DelayPromise:封装延迟完成逻辑的Task子类
  • TimerQueueTimer:线程池内部的定时器实例,负责在指定时间后触发DelayPromise的完成回调

这两个对象会被加入线程池的全局ConcurrentQueue,由线程池的定时器线程统一调度。

2. 未await导致的对象堆积

因为你没有用await等待Task.Delay完成,也没有处理返回的Task,所以每次循环迭代都会生成一对新的DelayPromise和TimerQueueTimer。这些对象会被线程池的全局队列强引用,GC会判定它们仍处于活跃状态,不会标记为可回收。

只有当定时器到期(25ms后),线程池才会从队列中移除对应的TimerQueueTimer,此时两个对象才会失去根引用,成为GC的回收目标。但在高负载场景下(比如10000次并发调用),短时间内会产生海量这类对象,在25ms的窗口期内,所有未到期的对象都会堆积在内存中,直接导致内存飙升。

3. GC不自动回收的核心原因

GC自动回收的前提是对象没有任何根引用。而这里的TimerQueueTimer被线程池的全局队列持有根引用,只要定时器未到期,这些对象就无法被回收。即便内存压力极高,GC触发时也会判定这些对象仍在活跃,不会进行回收。

手动触发GC时,大部分定时器已经到期,对应的对象已经失去根引用,因此GC能一次性回收堆积的对象。

4. Thread.Sleep的差异

Thread.Sleep(25)是让当前线程直接进入阻塞状态,不会创建任何额外的定时器或Task对象,没有多余内存分配。线程阻塞期间仅释放CPU资源,不会产生需要GC处理的对象,因此内存能维持稳定,GC只需处理业务逻辑本身的对象。

内容的提问来源于stack exchange,提问作者gnedevski

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.04 08:00:20