7

What is the thread safe way to dispose a lazy-initialized object in C#? Suppose I have the following Lazy construct:

Lazy<MyClass> lazy = new Lazy<MyClass>(() => MyClass.Create(), true);

Later, I might want to dispose the MyClass instance created. Most existing solutions recommend something like this:

if (lazy.IsValueCreated)
{
    lazy.Value.Dispose();
}

But as far as I can tell IsValueCreated does not hold any locks: https://referencesource.microsoft.com/#mscorlib/system/Lazy.cs,284

This means another thread may be in the process of initializing MyClass when we check for IsValueCreated. In that case we will observe IsValueCreated to be false, and end up leaking a resource. What is the right course of action here? Or have I missed some subtle detail?

Hiranya Jayathilaka
  • 7,180
  • 1
  • 23
  • 34
  • 2
    `This means another thread may be in the process of initializing MyClass`. I think this is a typical problem of any multi-threading application where resources are shared between the threads. You can build your own locks or take a good look at your object's lifetime. Even if you would be able to lock the `IsValueCreated`, you might still have an issue while accessing it in the middle of your dispose operation. – Stefan Jun 19 '18 at 19:24
  • 1
    Realistically the only shot you have at disposing of it is once you're sure no other threads could possibly be using it. If you can't make that assertion then one of those other threads could even *start* initializing it after you've already determined that no one is using it. That's inherent to the nature of disposable objects. There should be an "owner" of it that knows conclusively when no one else could possibly be using it anymore, and so it can be disposed of. If another thread might be using it, you aren't ready to dispose of it. – Servy Jun 19 '18 at 19:34
  • 2
    Whether `IsValueCreated` takes a lock or not is only one issue you might have. If this is your shot at disposing the object, if no thread is *currently* about to initialize it, if it were to do that later, after you've run the dispose-code (which thus didn't dispose of anything) you won't dispose of that. If the object *is* there when the dispose-code is running you will dispose of it, but the lazy-object will still hold a reference to the now disposed object. In short, you cannot run your dispose code until you've guaranteed no such thread is either executing or going to execute later. – Lasse V. Karlsen Jun 19 '18 at 19:37
  • 1
    How does it make sense to lazy-initialize a disposable resource? – John Wu Jun 19 '18 at 19:47
  • 3
    I don't see how whether a resource is managed or unmanaged relates to whether you want to defer instantiating it and ensuring that two threads cannot end up with separate instances of it. Rather, the problem I see here is that the design is disposing it when the op has no confidence that all threads are done with it. With that sort of problem, a marshaller class which can suitably lock around calls to get or return it. You can then choose yourself whether it is appropriate to dispose or even defer disposal for a short period of time. – Adam G Jun 19 '18 at 23:14

1 Answers1

1

I would set my lazy instance to null in Dispose() so that any further access would fail (e.g. throwing ObjectDisposedException). And then create a getter that handles this:

MyClass MyObj => lazy?.Value ?? throw new ObjectDisposedException(nameof(MyObj));

public void Dispose()
{
   var lazyBeforeDispose = Interlocked.Exchange(ref lazy, null);
   if (lazyBeforeDispose?.IsValueCreated ?? false) 
   {
      lazyBeforeDispose.Value.Dispose();
   }
}
Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
Maziar Taheri
  • 2,288
  • 22
  • 27
  • In this case you might also have to declare the `lazy` field as `volatile`, to prevent reordering of instructions. – Theodor Zoulias Jun 27 '23 at 08:31
  • `Interlocked.Exchange` takes care of that. See this for more info: https://stackoverflow.com/questions/1186515/interlocked-and-volatile/1186562#1186562 – Maziar Taheri Jun 28 '23 at 04:27
  • In the `MyObj` property, since the `lazy` is not `volatile` (or the [`Volatile.Read`](https://learn.microsoft.com/en-us/dotnet/api/system.threading.volatile.read) is not used) it is possible that the Jitter can move instructions that appear after reading the `lazy`, before reading the `lazy`. I am not sure what can be the consequences of this reordering in the worst case scenario, but I wouldn't risk it. – Theodor Zoulias Jun 28 '23 at 05:24