0

I got stuck in the weeds of how IDisposable and the GarbageCollector work.

Suppose you have an IDisposable object, which doesn't actually have an resources that it's holding onto (but the Dispose() method is going to do something when called). And suppose you declare it in the head of a using block, but don't actually interact with the object over the course of the block.

What guarantees do I have about how GarbageCollection will operate?

i.e.

using(new MyConceptuallyDisposableObject())
{
   DoSomeWork();
   await DoSomeAsyncWork();
}//PointX

Note that:

  • MyConceptuallyDisposableObject doesn't declare a finaliser / destructor.
    • (Assume that developers will never forget to using`.Dispose()` my object)
  • MyConceptuallyDisposableObject doesn't call GC.SuppressFinalise(this) anywhere.

Am I guaranteed that the object that I constructed will:

  1. Will not have .Dispose() called on it before PointX?
  2. Will have .Dispose() called on it at exactly PointX?
  3. Will not get GarbageCollected/Finalised at any point before PointX?
  4. Will not get GarbageCollected/Finalised before it has had .Dispose() called on it?

Suppose I then change my code to make MyConceptuallyDisposableObject call GC.SuppressFinalise(this) in its constructor. (Bearing in mind that there isn't any Destructor or Finaliser)

  1. Does that change any of the answers to the specific questions above?
  2. Does anything change in general, then?
  3. Does it mean that the GC never cleans up my object at all and I'll end up with a memory leak?

*Context:*

Posted for those who are inevitably curious, but PLEASE don't answer based suggesting other ways to achieve this or that I shouldn't do this. Right now, I'm much more invested in understanding the guts of the above concepts in their abstract sense, not discussing whether my initial attempt was sensible.

I want to write a DisposableAction() class, which accepts 2 Actions. One to perform when you construct it, and one to perform when you Dispose() it.

I thought I knew all the answers to the above (and that they were "Yes", "Yes", "Yes", "Yes", "No", "Almost nothing unless you're incredibly perf-sensitive", and "No".), but I've been trying to diagnose a bug which appears to contradict these beliefs.

Enigmativity
  • 113,464
  • 11
  • 89
  • 172
Brondahl
  • 7,402
  • 5
  • 45
  • 74
  • 2
    This feels like a XY Problem. There is clearly a bug that breaches your expectations. Could you show us a [mcve] of that bug? – mjwills Jul 22 '20 at 12:18
  • For 1 - no, there is no such guarantee since clearly some other code could call `Dispose` on the object (odd, sure, but possible). – mjwills Jul 22 '20 at 12:18
  • [Will the Garbage Collector call IDisposable.Dispose for me?](https://stackoverflow.com/questions/45036/will-the-garbage-collector-call-idisposable-dispose-for-me) – Pavel Anikhouski Jul 22 '20 at 12:19
  • Are you aware that "Point X" can be reached in other ways then sequentially stepping through your two lines of code, for example when the first line throws an exception? – nvoigt Jul 22 '20 at 12:30
  • 3 might be slightly more complicated than most people think. cbrumme's blog posts (mirror at https://www.cnblogs.com/tanglaoya321/archive/2013/06/07/3125442.html) are helpful here. For example `In other words, ‘this’ can be collected even while you are executing an instance method on that object.` In practical terms though, since you don't have a finalizer, it is hard to imagine this causing an issue for you. – mjwills Jul 22 '20 at 12:35
  • If an object has no finaliser, then `GC.SuppressFinalise(this)` is a no-op. It makes literally no difference (I mean it will be slightly slower to call it - but basically immeasurable). So 5, 6 and 7 are no. – mjwills Jul 22 '20 at 12:39
  • Do you understand that `using` is syntactic sugar, and the code that is generated behind the scenes (in terms of a `try finally` block)? – mjwills Jul 22 '20 at 12:40
  • @mjwills, re: other code calling `.Dispose()`. Nice outside-the-box thinking :D Assume that the constructor doesn't give a reference of itself to anything, and thus there's no "dangling" reference, unless it's done by `using`. – Brondahl Jul 22 '20 at 12:40
  • If you could share your `DisposableAction` code it may be useful. The kinds of mistakes people make with that kind of code are often quite simple so we may be able to spot them. – mjwills Jul 22 '20 at 12:40
  • @mjwills Re: "using is a try..catch..finally".. yes, that was my understanding. Do you mean that C# ***literally*** transpiles it into a trycatch block and then compiles *that*? I had assumed that the "try-catch-finally" point was only conceptual not literal? – Brondahl Jul 22 '20 at 12:42
  • `Do you mean that C#` https://mattwarren.org/2017/05/25/Lowering-in-the-C-Compiler/ / https://github.com/dotnet/roslyn/blob/master/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_UsingStatement.cs may be worth a read. – mjwills Jul 22 '20 at 12:46
  • 2 is not _guaranteed_ - for example `Dispose` won't be called if an exception was thrown in constructing `MyConceptuallyDisposableObject`. It also won't be called if the process is killed or the code in the `using` block never stops executing (as examples). – mjwills Jul 22 '20 at 12:47
  • `DisposableAction` is as simple as it could conceivably be :D ` public class DisposableAction : IDisposable { private readonly Action onDispose; public DisposableAction(Action onDispose) { this.onDispose = onDispose ?? throw new ArgumentNullException(nameof(onDispose)); //GC.SuppressFinalize(this); } public void Dispose() => onDispose(); }` – Brondahl Jul 22 '20 at 12:50
  • :) I simplified it further since the setup action wasn't the problem. We've now found the real problem (Deadlock in the threading code that this was attempting to control. For my money your link to the `Lowering` article and the Roslyn Rewriter constitutes a direct answer to this Q, since lines 279 - 303 make it unambiguous that a reference is still being held, answering all the questions about the possible interactions with `GC`. (and confirming what I'd previously understood) – Brondahl Jul 22 '20 at 13:17
  • Do you want to post that, and a statement of which questions it answers, plus some of your various good counters to #2, as a formal answer. – Brondahl Jul 22 '20 at 13:18
  • `make it unambiguous that a reference is still being held, answering all the questions about the possible interactions with GC` Again, I'd strongly recommend reading up on cbrumme's posts on GC, finalisation etc etc. Or Joe Duffy's. _In most scenarios it doesn't matter - but if you care about the details it may be of interest._ – mjwills Jul 22 '20 at 13:37
  • The answers to your 7 questions are yes, yes, yes, yes, no, no, no. – Lasse V. Karlsen Jul 22 '20 at 13:43
  • You can see an example of what the compiled code actually looks like here - https://sharplab.io/#v2:CYLg1APgAgTAjAWAFBQMwAJboMLoN7LpGYZQAs6AsgBQCU+hxTUADOtQHYCmA7ugCIBLAM4AHAPbCAhgCMANlzq1GTIgSSriAXxXodG4rrSYYAkROnyu6EOgCSQsZNkLd6zUWOOLLxfTzoAOLYAHQAygCuoqIATlzCwgBighxScoIAXooALgAWIrQA3Hq6zKQU3pJ++CUGRPpaQA (try removing the call to SuppressFinalize) – Lasse V. Karlsen Jul 22 '20 at 13:48
  • If you have a bug that contradicts my answers in my above comment, you should try to narrow down the bug and post about it here. I'm sure there is a logical explanation for it, you're just not seeing it. – Lasse V. Karlsen Jul 22 '20 at 13:50

1 Answers1

0

IDisposable and garbage collection are unrelated, except for the one situation where an object implements a finalizer that happens to call .Dispose().

Unless you know for sure, 100%, and explicitly, that an object has a finalizer that calls .Dispose() then you must call .Dispose() explicitly (or with a using) to ensure the object is disposed.

The answers to your questions:

  1. Yes.
  2. Yes.
  3. Yes.
  4. Yes.
  5. No.
  6. No.
  7. No.
Enigmativity
  • 113,464
  • 11
  • 89
  • 172
  • What @mjwills said. :) I'm not asking whether the `GC` specifically will call `.Dispose()` [I know that it definitely won't]. I asking about the total set of calls invoked by the Framework overall. – Brondahl Jul 22 '20 at 12:44
  • @mjwills - Sorry, I completely didn't see the `using` elephant in the room. I've updated my answers. – Enigmativity Jul 23 '20 at 01:13
  • @LasseV.Karlsen - See my note above. – Enigmativity Jul 23 '20 at 01:13