Search code examples
c#thread-safetysingletondispose

C# Singleton with a Disposable object


Suppose I have a Singleton that loads resources into memory when created, and performs operation on the data when callings its methods. Now suppose, that I want to have the ability to tell the Singleton to release those resources, as I don't expect to be using them in the near future, but also be able to load those resources back in, when the time comes. And I want it all to be thread safe.

What would be the best way to aproach this problem?

Would this example work?:

// Singleton implementation
...

private IDisposable resource;
private bool loadingResources;

private IDisposable Resource {
    get => resource ?? throw new CustomException();
}

// Method A
public void A() {
    var resource = Resource; // Throws CustomException if resource is null
    // Do stuff
}

// Method B
public void B() {
    var resource = Resource;
    // Do stuff
}

public void ReleaseResources() {
    if (resource != null)
        lock (thislock) {
            //resource.Dispose();
            resource = null;
        }
}

public void LoadResources() {
    if (!loadingResources && resource == null)
        lock (thislock)
            if (!loadingResources && resource == null)
            {
                loadingResources = true;
                // Load resources
                resource = CreateResource();
                loadingResources = false;
            }
}

Solution

  • I would suggest separating the resource handling from the actual usage. Assuming the resource requires disposal this could look something like:

        public class DisposableWrapper<T> where T : IDisposable
        {
            private readonly Func<T> resourceFactory;
            private T resource;
            private bool constructed;
            private object lockObj = new object();
            private int currentUsers = 0;
    
            public DisposableWrapper(Func<T> resourceFactory)
            {
                this.resourceFactory = resourceFactory;
            }
    
            public O Run<O>(Func<T, O> func)
            {
                lock (lockObj)
                {
                    if (!constructed)
                    {
                        resource = resourceFactory();
                        constructed = true;
                    }
                    currentUsers++;
                }
    
                try
                {
                    return func(resource);
                }
                catch
                {
                    return default;
                }
                finally
                {
                    Interlocked.Decrement(ref currentUsers);
                }
            }
    
            public void Run(Action<T> action)
            {
                lock (lockObj)
                {
                    if (!constructed)
                    {
                        resource = resourceFactory();
                        constructed = true;
                    }
                    currentUsers++;
                }
    
                try
                {
                    action(resource);
                }
                finally
                {
                    Interlocked.Decrement(ref currentUsers);
                }
            }
    
            public bool TryRelease()
            {
                lock (lockObj)
                {
                    if (currentUsers == 0 && constructed)
                    {
                        constructed = false;
                        resource.Dispose();
                        resource = default;
                        return true;
                    }
                    return false;
                }
            }
        }
    

    If the resource does not require disposal I would suggest to instead use lazy<T>. Releasing resources would simply mean replacing the existing lazy object with a new one. Letting the old object be cleaned up by the garbage collector.