为何Console.ReadLineAsync的可取消Task扩展会引发内存泄漏?
你的AsCancellable扩展方法引发内存泄漏的核心问题是未清理CancellationToken的注册回调,加上长期等待的Console.ReadLineAsync任务持有完整的对象引用链,具体拆解:
回调注册未释放:每次调用
AsCancellable时,都会通过token.Register()向CancellationToken注册一个回调函数,这个回调会引用TaskCompletionSource<T>(简称TCS)。如果你的_cancellationTokenSource是长期存活的(比如类级别的实例,未被取消),CancellationToken会一直持有这个回调的引用。而回调引用的TCS又被ContinueWith的延续任务绑定,延续任务又依附在未完成的Console.ReadLineAsync任务上——控制台无输入时,Console.ReadLineAsync会一直处于等待状态,不会完成,这就导致整个引用链(回调→TCS→延续任务→Console.ReadLineAsync任务)都无法被GC回收。每次循环都会新增一套这样的对象,内存自然持续增长。延续任务的持有问题:
ContinueWith创建的延续任务会一直绑定在原始的Console.ReadLineAsync任务上,只要原始任务不完成,这个延续任务就不会被销毁。加上回调的引用,整个对象图被牢牢持有,GC无法回收这些资源。嵌入式Linux的GC特性放大问题:嵌入式设备的GC触发阈值通常更高,后台GC频率更低,导致未回收的对象无法及时被清理,泄漏现象比桌面环境更明显。
修改AsCancellable方法,在任务完成后释放CancellationToken的注册回调,打破引用链:
public static async Task<T> AsCancellable<T>(this Task<T> task, CancellationToken token) { if (!token.CanBeCanceled) return await task.ConfigureAwait(false); using var cancelRegistration = token.Register(() => {}, useSynchronizationContext: false); var tcs = new TaskCompletionSource<T>(TaskCreationOptions.RunContinuationsAsynchronously); // 注册取消回调并保存实例 cancelRegistration = token.Register(() => tcs.TrySetCanceled(token), useSynchronizationContext: false); try { var completedTask = await Task.WhenAny(task, tcs.Task).ConfigureAwait(false); return await completedTask.ConfigureAwait(false); } finally { // 释放注册,移除回调引用 cancelRegistration.Dispose(); } }
或者用更简洁的Task.WhenAny方案,避免手动管理TCS的复杂逻辑:
public static async Task<T> AsCancellable<T>(this Task<T> task, CancellationToken token) { if (!token.CanBeCanceled) return await task.ConfigureAwait(false); using var cancelTaskSource = new TaskCompletionSource<bool>(); using var registration = token.Register(() => cancelTaskSource.TrySetResult(true), useSynchronizationContext: false); if (await Task.WhenAny(task, cancelTaskSource.Task).ConfigureAwait(false) == cancelTaskSource.Task) { token.ThrowIfCancellationRequested(); } return await task.ConfigureAwait(false); }
这样每次任务完成或取消后,都会释放CancellationToken的注册回调,打破引用链,避免对象被持续持有导致内存泄漏。
内容的提问来源于stack exchange,提问作者Ron Beyer

