将数组传入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
相关产品推荐
相关产品推荐

