这不是重复的 Task not garbage collected .虽然症状相似。
下面的代码是一个控制台应用程序,它创建一个用于 WinForms 的 STA 线程。任务通过使用 TaskScheduler.FromCurrentSynchronizationContext
获得的自定义任务调度程序发布到该线程,它只是在此处隐式包装了 WindowsFormsSynchronizationContext
的一个实例。
根据导致此 STA 线程结束的原因,最终任务 var terminatorTask = Run(() => Application.ExitThread())
,计划在 WinformsApartment.Dispose
方法,可能并不总是有机会执行。不管怎样,我相信这个任务仍然应该被垃圾收集,但事实并非如此。为什么?
这是一个独立的示例,说明(s_debugTaskRef.IsAlive
在结束时为 true
),使用 .NET 4.8 进行测试,包括调试和发布:
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace ConsoleTest
{
class Program
{
// entry point
static async Task Main(string[] args)
{
try
{
using (var apartment = new WinformsApartment(() => new Form()))
{
await Task.Delay(1000);
await apartment.Run(() => Application.ExitThread());
}
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
Environment.Exit(-1);
}
GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced);
GC.WaitForPendingFinalizers();
Console.WriteLine($"IsAlive: {WinformsApartment.s_debugTaskRef.IsAlive}");
Console.ReadLine();
}
}
public class WinformsApartment : IDisposable
{
readonly Thread _thread; // the STA thread
readonly TaskScheduler _taskScheduler; // the STA thread's task scheduler
readonly Task _threadEndTask; // to keep track of the STA thread completion
readonly object _lock = new object();
public TaskScheduler TaskScheduler { get { return _taskScheduler; } }
public Task AsTask { get { return _threadEndTask; } }
/// <summary>MessageLoopApartment constructor</summary>
public WinformsApartment(Func<Form> createForm)
{
var schedulerTcs = new TaskCompletionSource<TaskScheduler>();
var threadEndTcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
// start an STA thread and gets a task scheduler
_thread = new Thread(_ =>
{
try
{
// handle Application.Idle just once
// to make sure we're inside the message loop
// and the proper synchronization context has been correctly installed
void onIdle(object s, EventArgs e) {
Application.Idle -= onIdle;
// make the task scheduler available
schedulerTcs.SetResult(TaskScheduler.FromCurrentSynchronizationContext());
};
Application.Idle += onIdle;
Application.Run(createForm());
threadEndTcs.TrySetResult(true);
}
catch (Exception ex)
{
threadEndTcs.TrySetException(ex);
}
});
async Task waitForThreadEndAsync()
{
// we use TaskCreationOptions.RunContinuationsAsynchronously
// to make sure thread.Join() won't try to join itself
Debug.Assert(Thread.CurrentThread != _thread);
await threadEndTcs.Task.ConfigureAwait(false);
_thread.Join();
}
_thread.SetApartmentState(ApartmentState.STA);
_thread.IsBackground = true;
_thread.Start();
_taskScheduler = schedulerTcs.Task.Result;
_threadEndTask = waitForThreadEndAsync();
}
// TODO: it's here for debugging leaks
public static readonly WeakReference s_debugTaskRef = new WeakReference(null);
/// <summary>shutdown the STA thread</summary>
public void Dispose()
{
lock(_lock)
{
if (Thread.CurrentThread == _thread)
throw new InvalidOperationException();
if (!_threadEndTask.IsCompleted)
{
// execute Application.ExitThread() on the STA thread
var terminatorTask = Run(() => Application.ExitThread());
s_debugTaskRef.Target = terminatorTask; // TODO: it's here for debugging leaks
_threadEndTask.GetAwaiter().GetResult();
}
}
}
/// <summary>Task.Factory.StartNew wrappers</summary>
public Task Run(Action action, CancellationToken token = default(CancellationToken))
{
return Task.Factory.StartNew(action, token, TaskCreationOptions.None, _taskScheduler);
}
public Task<TResult> Run<TResult>(Func<TResult> action, CancellationToken token = default(CancellationToken))
{
return Task.Factory.StartNew(action, token, TaskCreationOptions.None, _taskScheduler);
}
public Task Run(Func<Task> action, CancellationToken token = default(CancellationToken))
{
return Task.Factory.StartNew(action, token, TaskCreationOptions.None, _taskScheduler).Unwrap();
}
public Task<TResult> Run<TResult>(Func<Task<TResult>> action, CancellationToken token = default(CancellationToken))
{
return Task.Factory.StartNew(action, token, TaskCreationOptions.None, _taskScheduler).Unwrap();
}
}
}
我怀疑这可能是 .NET Framework 错误。我目前正在调查它,我会发布我可能发现的内容,但也许有人可以立即提供解释。
最佳答案
好吧,看来 WindowsFormsSynchronizationContext
在这里没有得到正确处理。不确定这是错误还是“功能”,但以下更改确实修复了它:
SynchronizationContext syncContext = null;
void onIdle(object s, EventArgs e) {
Application.Idle -= onIdle;
syncContext = SynchronizationContext.Current;
// make the task scheduler available
schedulerTcs.SetResult(TaskScheduler.FromCurrentSynchronizationContext());
};
Application.Idle += onIdle;
Application.Run(createForm());
SynchronizationContext.SetSynchronizationContext(null);
(syncContext as IDisposable)?.Dispose();
现在 IsAlive
是 false
并且任务得到了正确的 GC。注释掉上面的(syncContext as IDisposable)?.Dispose()
,IsAlive
又回到了true
。
已更新,如果有人使用类似的模式(我自己将其用于自动化),我现在建议显式控制 WindowsFormsSynchronizationContext
的生命周期和处置:
public class WinformsApartment : IDisposable
{
readonly Thread _thread; // the STA thread
readonly TaskScheduler _taskScheduler; // the STA thread's task scheduler
readonly Task _threadEndTask; // to keep track of the STA thread completion
readonly object _lock = new object();
public TaskScheduler TaskScheduler { get { return _taskScheduler; } }
public Task AsTask { get { return _threadEndTask; } }
/// <summary>MessageLoopApartment constructor</summary>
public WinformsApartment(Func<Form> createForm)
{
var schedulerTcs = new TaskCompletionSource<TaskScheduler>();
var threadEndTcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
// start an STA thread and gets a task scheduler
_thread = new Thread(_ =>
{
try
{
// handle Application.Idle just once
// to make sure we're inside the message loop
// and the proper synchronization context has been correctly installed
void onIdle(object s, EventArgs e)
{
Application.Idle -= onIdle;
// make the task scheduler available
schedulerTcs.SetResult(TaskScheduler.FromCurrentSynchronizationContext());
};
Application.Idle += onIdle;
Application.Run(createForm());
threadEndTcs.TrySetResult(true);
}
catch (Exception ex)
{
threadEndTcs.TrySetException(ex);
}
});
async Task waitForThreadEndAsync()
{
// we use TaskCreationOptions.RunContinuationsAsynchronously
// to make sure thread.Join() won't try to join itself
Debug.Assert(Thread.CurrentThread != _thread);
try
{
await threadEndTcs.Task.ConfigureAwait(false);
}
finally
{
_thread.Join();
}
}
_thread.SetApartmentState(ApartmentState.STA);
_thread.IsBackground = true;
_thread.Start();
_taskScheduler = schedulerTcs.Task.Result;
_threadEndTask = waitForThreadEndAsync();
}
// TODO: it's here for debugging leaks
public static readonly WeakReference s_debugTaskRef = new WeakReference(null);
/// <summary>shutdown the STA thread</summary>
public void Dispose()
{
lock (_lock)
{
if (Thread.CurrentThread == _thread)
throw new InvalidOperationException();
if (!_threadEndTask.IsCompleted)
{
// execute Application.ExitThread() on the STA thread
var terminatorTask = Run(() => Application.ExitThread());
s_debugTaskRef.Target = terminatorTask; // TODO: it's here for debugging leaks
_threadEndTask.GetAwaiter().GetResult();
}
}
}
/// <summary>Task.Factory.StartNew wrappers</summary>
public Task Run(Action action, CancellationToken token = default(CancellationToken))
{
return Task.Factory.StartNew(action, token, TaskCreationOptions.None, _taskScheduler);
}
public Task<TResult> Run<TResult>(Func<TResult> action, CancellationToken token = default(CancellationToken))
{
return Task.Factory.StartNew(action, token, TaskCreationOptions.None, _taskScheduler);
}
public Task Run(Func<Task> action, CancellationToken token = default(CancellationToken))
{
return Task.Factory.StartNew(action, token, TaskCreationOptions.None, _taskScheduler).Unwrap();
}
public Task<TResult> Run<TResult>(Func<Task<TResult>> action, CancellationToken token = default(CancellationToken))
{
return Task.Factory.StartNew(action, token, TaskCreationOptions.None, _taskScheduler).Unwrap();
}
}
关于c# - 任务不会被垃圾收集,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/57626161/