Search code examples
c#.netmultithreadingdisposeshutdown

Guarantee that Dispose is called "when the process is ending"


There is a possibly-shared resource, X associated with foreground threads, that is used by two instances (Y, Z) and supplied as a dependency. Because X may be shared it cannot be Disposed of by either usage site (Y or Z).

The obvious solution would be to ensure that X is manually Disposed 'at some appropriate time' after it is no longer used: in this case it can be assumed this implies that X is no longer strongly-reachable. However, in this case there is a [buggy] client that does not do so.

Since X has an associated foreground thread this can keep the process from terminating (completely?).

Is there a way that X - without external dependencies, or even knowing if such is in an 'executable' or 'Windows Service' - ensure that a call to Dispose occurs before/as the process is terminating?

Is there a more intelligent way to handle such an (untracked) shared resource which may have one or more consumer(s)?

Currently this is relying on the finalizer but, as advertised, such is .. non-reliable.


Solution

  • This does not call dispose on X but it will tell you when all X have been collected by the GC (And forces a GC every 5 minutes)

    public sealed class CollectionWatcher<T>
    {
        private readonly Func<T> _objectFactory;
        private readonly List<WeakReference> _references;
        private readonly System.Timers.Timer _timer;
        private readonly Object _lockObject = new Object();
    
        public CollectionWatcher(Func<T> objectFactory, double minutesBetweenChecks = 5)
        {
            _objectFactory = objectFactory;
            _references = new List<WeakReference>();
            _timer = new System.Timers.Timer();
            _timer.Interval = TimeSpan.FromMinutes(minutesBetweenChecks).TotalMilliseconds;
            _timer.AutoReset = true;
            _timer.Elapsed += TimerElapsed;
        }
    
        public T GetObjectBecauseICantBeTrustedToDisposeCorrectly()
        {
            var result = _objectFactory();
            var wr = new WeakReference(result);
            lock (_lockObject)
            {
                _references.Add(wr);
                _timer.Start();
            }
    
            return result;
        }
    
        public event EventHandler AllItemsCollected;
    
        private void OnAllItemsCollected()
        {
            AllItemsCollected?.Invoke(this, EventArgs.Empty);
        }
    
    
        private void TimerElapsed(object sender, ElapsedEventArgs e)
        {
            bool allItemCollected = false;
    
            GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced, false);
            lock (_lockObject)
            {
                foreach (var reference in _references.Where(x => !x.IsAlive).ToList())
                {
                    _references.Remove(reference);
                }
    
                if (_references.Count == 0)
                {
                    _timer.Stop();
                    allItemCollected = true;
                }
            }
    
            if (allItemCollected)
            {
                OnAllItemsCollected();
            }
        }
    }
    

    You can't just call Dispose() on T because by the time .IsAlive is false the finalizer has already been called on T.

    I am going to keep thinking on this, maybe there could be other solutions.