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

将数组传入Task后内存未释放的原因及解决方法

数组内存未释放的原因及解决方案

示例代码

internal class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Hello, World!");

        for (int i = 0; i < 10; i++)
        {
            ProcessMessage(i);
        }

        Console.ReadLine();
    }

    private static void ProcessMessage(int i)
    {
        int[]? array = new int[10000000];
        for (int j = 0; j < array.Length; j++)
        {
            array[j] = i;
        }

        Task.Run(() => ProcessArray(array, i)).ConfigureAwait(false);
    }

    private static void ProcessArray(int[]? myArray, int number)
    {
        if (myArray == null) return;
        Console.WriteLine($"[{Environment.CurrentManagedThreadId}] Start task");
        Console.WriteLine($"[{Environment.CurrentManagedThreadId}] {myArray[0]}");
        Thread.Sleep((10 - number) * 1000);
        Console.WriteLine($"[{Environment.CurrentManagedThreadId}] {myArray[0]}");
        myArray = null;
        return;
    }
}

问题描述

我发现数组的内存并未被释放,这是为什么?该如何解决?
我无法通过await任务后将array设为null的方式处理。
这段代码只是示例,实际场景中我有一个顺序执行的事件处理器,需要并行处理事件。在事件处理器中我从ReadOnlyMemory<byte>获取数组,且必须在事件处理器返回前完成数组转换,无法在Task内部进行ReadOnlyMemory<byte>到数组的转换。


内存未释放的原因

  • ProcessMessage中创建的数组array被Task.Run的lambda表达式捕获,形成闭包。只要后台任务未执行完毕,闭包就会持有数组的强引用,GC无法回收该数组。
  • ProcessArray中的myArray = null仅将方法参数的引用置空,并未改变闭包中持有的array引用,因此对内存回收无帮助。
  • 任务执行时长为(10 - number)*1000毫秒,number从0到9,最长任务会运行10秒,期间所有对应数组都会被持有,无法被GC回收。

解决方案

方案1:使用弱引用传递数组

将数组包装为WeakReference<int[]>,闭包持有弱引用后,只要无其他强引用,GC可随时回收数组。修改后代码如下:

private static void ProcessMessage(int i)
{
    int[]? array = new int[10000000];
    for (int j = 0; j < array.Length; j++)
    {
        array[j] = i;
    }
    var weakArray = new WeakReference<int[]>(array);
    // 主动置空强引用,让GC可在任务无需数组时回收
    array = null;
    Task.Run(() => ProcessArray(weakArray, i)).ConfigureAwait(false);
}

private static void ProcessArray(WeakReference<int[]> weakArray, int number)
{
    if (!weakArray.TryGetTarget(out var myArray))
    {
        Console.WriteLine($"[{Environment.CurrentManagedThreadId}] 数组已被回收");
        return;
    }
    Console.WriteLine($"[{Environment.CurrentManagedThreadId}] Start task");
    Console.WriteLine($"[{Environment.CurrentManagedThreadId}] {myArray[0]}");
    Thread.Sleep((10 - number) * 1000);
    // 再次检查数组是否存在
    if (weakArray.TryGetTarget(out myArray))
    {
        Console.WriteLine($"[{Environment.CurrentManagedThreadId}] {myArray[0]}");
    }
    else
    {
        Console.WriteLine($"[{Environment.CurrentManagedThreadId}] 数组已被回收");
    }
}

注意:此方案下任务执行过程中数组可能被GC回收,需在使用前检查可用性。若业务逻辑要求数组全程可用,该方案不适用。

方案2:使用对象池复用数组

针对固定大小的数组,使用ArrayPool<int>.Shared(或自定义对象池)复用数组,避免频繁创建和回收大数组,从根源降低内存压力:

private static void ProcessMessage(int i)
{
    // 从对象池租借数组
    int[] array = ArrayPool<int>.Shared.Rent(10000000);
    try
    {
        for (int j = 0; j < array.Length; j++)
        {
            array[j] = i;
        }
        // 传递数组和实际需要的长度(Rent可能返回更大的数组)
        Task.Run(() => ProcessArray(array, i, 10000000)).ConfigureAwait(false);
    }
    finally
    {
        // 不能在此归还,需等任务执行完毕后再归还
    }
}

private static void ProcessArray(int[] myArray, int number, int actualLength)
{
    try
    {
        Console.WriteLine($"[{Environment.CurrentManagedThreadId}] Start task");
        Console.WriteLine($"[{Environment.CurrentManagedThreadId}] {myArray[0]}");
        Thread.Sleep((10 - number) * 1000);
        Console.WriteLine($"[{Environment.CurrentManagedThreadId}] {myArray[0]}");
    }
    finally
    {
        // 任务完成后归还数组到对象池
        ArrayPool<int>.Shared.Return(myArray);
    }
}

核心逻辑:复用数组,手动管理生命周期,避免GC频繁回收大对象,适合高频创建大数组的场景。注意归还数组后不可再使用,否则会导致数据错乱。

方案3:跟踪任务完成后释放引用

若必须保留强引用至任务完成,但无法在ProcessMessage中await任务,可通过全局任务集合+后台线程跟踪任务完成状态,完成后置空引用:

// 线程安全的全局任务集合
private static readonly ConcurrentBag<Task> _runningTasks = new ConcurrentBag<Task>();

static Program()
{
    // 启动后台线程清理已完成的任务
    ThreadPool.QueueUserWorkItem(_ =>
    {
        while (true)
        {
            Thread.Sleep(1000);
            // 移除已完成的任务
            foreach (var task in _runningTasks.Where(t => t.IsCompleted).ToList())
            {
                _runningTasks.TryTake(out _);
            }
        }
    });
}

private static void ProcessMessage(int i)
{
    int[]? array = new int[10000000];
    for (int j = 0; j < array.Length; j++)
    {
        array[j] = i;
    }
    var task = Task.Run(() =>
    {
        try
        {
            ProcessArray(array, i);
        }
        finally
        {
            // 任务完成后置空引用,GC可回收数组
            array = null;
        }
    }).ConfigureAwait(false);
    _runningTasks.Add(task);
}

// ProcessArray方法保持不变
private static void ProcessArray(int[]? myArray, int number)
{
    if (myArray == null) return;
    Console.WriteLine($"[{Environment.CurrentManagedThreadId}] Start task");
    Console.WriteLine($"[{Environment.CurrentManagedThreadId}] {myArray[0]}");
    Thread.Sleep((10 - number) * 1000);
    Console.WriteLine($"[{Environment.CurrentManagedThreadId}] {myArray[0]}");
    return;
}

此方案中,任务完成后在finally块置空array引用,GC即可回收数组,后台线程负责清理已完成的任务,避免集合无限膨胀。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.19 18:24:54