强制终结器顺序

3

总览

我需要绑定一个具有4个主要函数的本地API:

void ActivateEngine();
int CreateModule();
void DestroyModule(int id);
void TerminateEngine();

文档说明应该在调用CreateModuleDestroyModule时使用ActivateEngineTerminateEngine。使用方法应该类似于:

void foo()
{
    ActivateEngine();

    int module1 = CreateModule();
    int module2 = CreateModule();

    ...

    DestroyModule(module2);
    DestroyModule(module1);

    TerminateEngine();
}

为了实现这一点,我创建了两个.NET对象,即EngineModule,两者都使用DllImport属性绑定到本机API。 Engine对象作为单例模式,绑定到ActivateEngineTerminateEngineModule对象用于在Engine中创建许多实例,并绑定到本机API中的CreateModuleDestroyModule
遇到的问题:
我已经按照用户可以直接创建Modules的方式实现了很多内容,而不需要太关注Engine或对象的生命周期(即我不想强制用户在不再使用时处理对象)。
为此,我在Engine对象中使用了一个指向所有创建的ModulesWeakReference列表。

请看我简化后的代码这里

问题在于当应用程序结束时,终结器以不确定的方式被调用,并且即使Module的终结器尚未被调用,WeakReference目标已经为null并且参数trackResurrection设置为true。

在我的情况下,代码记录如下:

ActivateEngine() ...
CreateModule() ==> 0 ...
CreateModule() ==> 1 ...
DestroyModule(1) ...
  Ooops ... Can't dispose registered module because WeakReference to it is already null ...
  Ooops ... Can't dispose registered module because WeakReference to it is already null ...
TerminateEngine() ...
DestroyModule(0) ...

当然,这是不恰当的顺序。
问题
如何强制所有的ModuleEngine之前完成?
我真的不想强迫最终用户调用Module对象上的Dispose方法,也不想保留对已创建的Module的强引用,以便在代码中不再引用时自动消失。例如:
 processing
 {
     var module = new Module();
     ...
 }

 foo()
 {
     processing();
     GC.Collect(); // If using strong references 'module' is gonna be kept alive (that's not smart)  
 }

我查看了以下使用 ConditionalWeakTable 的线程:

但我不明白这如何帮助我解决我的问题。


你可以检查一下你提供的简化版代码链接吗? - Rob
@Rob 抱歉,我修改了链接到以下(问题已编辑为新链接) - CitizenInsane
1
非常不清楚你希望如何使用WeakReference。你至少需要一个计数器,一个简单的静态整数。对于每个CreateModule()调用,将其递增,对于每个DetroyModule()调用,将其递减。当它达到0时,您可以安全地调用TerminateEngine()。 - Hans Passant
强烈建议您实际上将对象设置为“IDisposable”。处理用于确定性资源清除,而终结器则是设计非确定性的。您甚至不知道它是否会在给定对象上调用,更别说调用的顺序了。只要用户确实处理对象,具有可处理对象就可以轻松解决这个问题。 - Servy
@Servy ... 是的 ... "只要用户实际上处理了对象" ... 这确实是问题,"用户" ... :) - CitizenInsane
@CitizenInsane,对于C#程序员来说,如果你没有正确处理可释放资源,那么你将会遇到问题是众所周知的。这是一个合理的期望。像你正在寻找的解决方案很可能会出现错误,即使它在一些常见情况下能够正常工作,也会导致用户无论做什么都不能依赖它正常工作,这是一个重大问题。 - Servy
4个回答

3

这更像是一个特定情况下的解决方法,而不是针对一般问题的解决方案:

将终止引擎的责任分配给引擎单例和模块对象。

创建一个共享计数器,通过Interlocked方法(或本地等效方法)进行更新。这可以是static volatile int字段或未管理的内存块。

int应计算您的应用程序维护的对引擎的“引用”数量。在每个构造函数中原子地递增计数器,在每个析构函数中递减计数器。将计数器减少到零的唯一析构函数还调用TerminateEngine()(并释放共享计数器)。

引擎对象也必须算作“引用”,以防用户让所有Module对象被垃圾回收,但随后开始创建新模块。否则,引擎将过早销毁。


谢谢@Christian,我找到了一个使用WeakEvents的解决方案,但这是一个不错的解决方法,我可以在当前情况下使用它。 - CitizenInsane

3
我认为你不能用你想要的方法来解决这个问题。最终化的顺序是不确定的,所以无法知道你的单例引擎 Engine 或一个 Module 会先被最终化。
我建议你移除你的 Engine 单例(把 Engine 作为一个静态类保留,但不允许任何实例,只在静态方法中使用它进行引擎初始化和终止),以及模块的注册,并使用一个静态原子计数器,沿着 @Christian-Klauser 的答案,当模块构造函数增加时(incremented),在 finalizer 中减去 (decremented)。当原子计数器从 0 变为 1 时,你可以通过调用内部的静态 Engine 方法激活引擎,同样地,当模块计数降至 0 时,终止引擎。
我还建议你在使用 Module 类时要求用户使用 using 机制。

感谢您的回答,@Rob。如果有兴趣,请查看下面使用WeakEvents的解决方案。 - CitizenInsane

2
正如其他答案和评论所说,您需要实现某种形式的引用计数。以下是我尝试做到这一点的方法(当您发布答案时,我正在处理此问题)。它仍然使用单例Engine(现在没有这个要求,您可以将其转换为静态类并进行最小更改),但是调用者需要调用AddRefrence()ReleaseRefrence()让引擎知道如果计数达到1或0,则需要设置或拆除API。 Module支持在调用Dispose()时释放它的引用,或者在类被终结时释放它的引用。
using System.Threading;

namespace FinalizerOrder
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;

    class Engine
    {
        private Engine()
        {
            //ActivateEngine() is no longer called here.
        }

        private readonly static Engine _singleton = new Engine(); //Now that the constructor is empty we can initialize immediately.
        private readonly static object _syncLock = new object();
        private static volatile int _counter = 0;

        public static Engine Singleton
        {
            get { return _singleton; }
        }

        public void AddRefrence()
        {
            lock (_syncLock)
            {
                _counter++;
                if (_counter < 0)
                    throw new InvalidOperationException("ReleaseRefrence() was called more times than AddRefrence()");

                if(_counter == 1)
                    Debug.WriteLine("ActivateEngine() ...");
            }
        }

        public void ReleaseRefrence()
        {
            lock (_syncLock)
            {
                _counter--;

                if (_counter < 0)
                    throw new InvalidOperationException("ReleaseRefrence() was called more times than AddRefrence()");

                if (_counter == 0)
                {
                    Debug.WriteLine("TerminateEngine() ...");
                }
            }
        }
    }

    class Module : IDisposable
    {
        public Module()
        {
            Engine.Singleton.AddRefrence();

            _id = _counter++;
            Debug.WriteLine("CreateModule() ==> {0} ...", _id);

        }

        private readonly int _id;
        private static int _counter;

        ~Module()
        {
            Dispose(false);   
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        private bool _disposed = false;

        protected void Dispose(bool disposing)
        {
            if(_disposed)
                return;

            _disposed = true;                

            if (disposing)
            {
                //Nothing to do here, no IDisposeable objects.
            }

            Debug.WriteLine("DestroyModule({0}) ...", _id);
            Engine.Singleton.ReleaseRefrence();
        }
    }

    internal class Program
    {
        private static void Main()
        {
            Test();
            GC.Collect(3, GCCollectionMode.Forced);
            GC.WaitForPendingFinalizers();
            Test();

        }

        private static void Test()
        {
            var module1 = new Module();
            var module2 = new Module();

            GC.KeepAlive(module2);
            GC.KeepAlive(module1);
        }
    }
}

感谢您使用引用计数编写解决方案...很抱歉在中间发布了帖子...无论如何,这是解决问题的好方法和简单的解决方法。 - CitizenInsane
这如何确保模块按照它们创建的相反顺序被释放? - Servy
@Servy 在原帖中,我只看到在第一次调用 CreateModule() 之前必须调用 ActivateEngine(),并且在每个已创建实例的 DestroyModule(int) 被调用后必须调用 TerminateEngine()。我没有看到任何关于在 ActivateEngine()TerminateEngine() 之间调用 DestroyModule(int) 的顺序有任何影响的内容。 - Scott Chamberlain
哦,如果在您的应用程序中创建一个模块、处理它,然后再创建另一个模块,会导致使用已释放的“Engine”。 - Servy
@Servy 不是这样的,顺序应该是 ActivateEngine()CreateModule()DestroyModule(0)TerminateEngine()ActivateEngine()CreateModule()。因此,当最后一个模块被销毁时,它会终止引擎,然后在下一个模块进入时创建一个新引擎(我在底部的测试代码中测试了这一点,在 GC 清理之间调用了两次 Test())。 - Scott Chamberlain

1

我最终使用了FastSmartWeakEvent模式。

这是一种通用解决方案,易于阅读和理解。

这里查看更新的示例代码。


该死...在处理其他测试用例后,这是个陷阱...WeakEvents是弱引用...它们也会导致不可预测的终结器顺序...Christian的建议和@Scott的实现肯定是最合适的(到目前为止对所有测试用例都有效)。 - CitizenInsane

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