任务未被垃圾回收。

3

这并不是任务未被垃圾回收的重复。虽然症状相似。

以下代码为一个控制台应用程序,创建了一个STA线程以供WinForms使用。任务是通过TaskScheduler.FromCurrentSynchronizationContext获得的自定义任务计划程序发布到该线程中的,此处只是隐式地包装了一个WindowsFormsSynchronizationContext实例。

根据导致STA线程结束的原因,最后一个任务var terminatorTask = Run(() => Application.ExitThread())(在WinformsApartment.Dispose方法中调度)可能并没有总是有机会执行。尽管如此,我认为这个任务仍然应该被垃圾回收,但它没有。为什么?

下面是一个自包含的示例,演示了这一点(在.NET 4.8上进行测试,调试和发布都是s_debugTaskRef.IsAlivetrue):

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的错误。我目前正在调查,如果我有所发现,我会发布出来,但也许有人可以立即提供解释。


1
你能把 try { ... } 里面的所有代码,也就是using部分单独移到一个方法中再次测试吗?我只是想排除调试器或Jitter引入的隐藏临时变量导致引用保持活动状态的可能性。 - Lasse V. Karlsen
1
我的怀疑是你在两次告诉应用程序退出线程。如果第一个实际上停止了它的消息泵,那么WinformsApartment.Dispose中的第二个调用将永远不会被安排。因此代表它的任务将一直挂起。 - canton7
1
还是应该进行垃圾回收”--我认为这完全不正确。任务的操作坐落在消息队列中,但没有任何东西在推动那个队列。任务不知道正在推动队列的事物已经死亡-就它所知道的而言,队列只是需要一段时间才能被推动。” - canton7
1
不错的类!小的风格改变:我会将字段 _threadEndTask 重命名为 _completion,并将属性 AsTask 改为 Completion,以便跟随类似 Dataflow 属性的示例。 :-) - Theodor Zoulias
1
@TheodorZoulias 谢谢!这是一个好建议 :) - noseratio - open to work
显示剩余11条评论
1个回答

2

看起来这里WindowsFormsSynchronizationContext没有被正确地处理。不确定是一个bug还是"feature",但以下更改可以解决:

原始答案翻译成中文为"最初的回答"

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();

现在,IsAlivefalse,任务被正确地垃圾回收了。请将上面的(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();
    }
}

@canton7,就是这样。要了解更多细节,我需要查看.NET源代码,但我想象他们在通过/之后从线程卸载它时不会调用“WindowsFormsSynchronizationContext.Dispose”。 - noseratio - open to work
WindowsFormSynchronizationContext.Dispose 只是处理它发布内容的控件(可能是您的 Form?)。因此,很有可能是 Form 没有被处理?这是有道理的,因为我认为它是 Form 在这里持有消息队列。 - canton7
@canton7,我已将上面的代码更改为var form = createForm(); Application.Run(form); form.Dispose(),但仍然需要显式调用WindowsFormsSynchronizationContext.Dispose才能正常工作。看起来像是一个bug。 - noseratio - open to work
请查看此处:https://referencesource.microsoft.com/#System.Windows.Forms/winforms/Managed/System/WinForms/WindowsFormsSynchronizationContext.cs,70。您还可能需要执行“SynchronizationContext.SetSynchronizationContext(null);”,因为SynchronizationContext持有对“Form”的引用。 - canton7
啊哈,虽然 Dispose() 也会设置 controlToSendTo = null;,这表明可能还有其他东西保留了你的 SynchronizationContext - canton7
@canton7,实际上即使没有 SynchronizationContext.SetSynchronizationContext(null) 也可以工作,但是 (syncContext as IDisposable)?.Dispose() 是至关重要的。 - noseratio - open to work

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接