Search code examples
c#weak-referencesfinalizer

Forcing finalizers order


General overview

I need to bind with a native API that has 4 mains functions:

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

And the documentation states that ActivateEngine and TerminateEngine should surround any call to CreateModule and DestroyModule. That is usage should be something like:

void foo()
{
    ActivateEngine();

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

    ...

    DestroyModule(module2);
    DestroyModule(module1);

    TerminateEngine();
}

To do this I have created two .NET objects, namely Engine and Module both binded to the native API using the DllImport attribute.

Engine object acts as singleton and is binded to ActivateEngine and TerminateEngine.

Module object is used to create many instances within the Engine and is binded to CreateModule and DestroyModule in the native API.

Encountered issue

I have implemented things in a way that users can create Modules directly without carrying too much about the Engine or about the lifetime of the objects (i.e. I don't [And I don't want to] force users to dispose objects when no longer used).

To do this I have used a list of WeakReference in Engine object that points to all created Modules.

See my simplified code here.

The issue is that when application's end, finalizer are called in non determistic way and WeakReference targets are already null even if finalizer of Module has not been called yet and that parameter trackResurrection is set to true.

In my case the code logs the following:

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) ...

Which of course is inappropriate order.

Question

How can force all Module to be finalized before the Engine ?

I truly don't want to force end-users to call Dispose method on Module objects and I also don't want to keep strong references to created Module so that objects can disappear automatically when no longer referenced in the code. Example:

 processing
 {
     var module = new Module();
     ...
 }

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

I have looked to following threads using ConditionalWeakTable:

But I don't understand how this can help in my situation.


Solution

  • As the other answers and comments have said you need to implement some form of reference counting. Here is my attempt at doing it (I was working on this when you posted your answer), It still uses a singleton Engine (there is no requirement now to do so, you could make it a static class with minimal changes), however callers need to call AddRefrence() and ReleaseRefrence() to let the engine know if it needs to setup or tear down the API when the count hits 1 or 0 respectively.

    Module supports releasing it's reference on calling Dispose() or when the class is finalized.

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