Reputation:
I am new to programming. I am studying chapter 14 of John Sharp's Microsoft Visual C # Step by Step 9ed. And I do not understand a number of points.
The author writes:
...it (finalizer) can run anytime after the last reference to an object has disappeared. So it is possible that the finalizer might actually be invoked by the garbage collector on its own thread while the Dispose method is being run, especially if the Dispose method has to do a significant amount of work.
1) Here I have a first question, how is this possible? After all, GC CLR destroys the object as correctly noticed when there are no more links to it. But if there are no references, how then can simultaneously still work the method of an object (Dispose()) to which nothing else refers? Yes, there are no links, but the method is not completed and GC CLR will try to delete the method object that still works?
Further, the author proposes to use lock (this) to circumvent this problem (parallel calls to Dispose ()) and clarifies that this can be detrimental to performance by immediately suggesting another strategy described earlier in the chapter.
class Example : IDisposable
{
private Resource scarce; // scarce resource to manage and dispose
private bool disposed = false; // flag to indicate whether the resource
// has already been disposed
...
~Example()
{
this.Dispose(false);
}
public virtual void Dispose()
{
this.Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!this.disposed)
{
if (disposing)
{
// release large, managed resource here
...
}
// release unmanaged resources here
...
this.disposed = true;
}
}
//other methods
}
2) I understand how it works and why it is needed, but I have problems with mentioning exactly the parallel execution of this.Dispose(true)
and this.Dispose(false)
.
Which in the proposed last solution will not allow GC CLR to call the this.Dispose(false)
method in its thread in parallel through the destructor in the same way as before while this.Dispose(true)
will still be executed previously launched explicitly?
At first glance, this is a GC.SuppressFinalize
(this) construct, but it should work only after the end of this.Dispose (true), and by condition it is executed long enough for GC CLR in its thread to start the deconstructor and this.Dispose(false)
.
As I understand it, nothing prevents and we get only a repeat of discarding unmanaged resources (files, connections to databases, and so on), but we don’t get a repeat of discarding managed resources (for example, a large multidimensional array).
It turns out that it is permissible to repeatedly drop unmanaged resources and it is unacceptable to repeatedly discard managed resources? And this is better than using the lock (this) construct?
Upvotes: 6
Views: 1708
Reputation: 40323
it is possible that the finalizer might actually be invoked by the garbage collector on its own thread while the Dispose method is being run
(...)
how is this possible?
A local variable that references an object, which will no longer be used in the method, does not prevent garbage collection.
Please refer to When does an object become available for garbage collection?.
In the article, Raymond shows that an object can become elegible for garbage collection during the execution of one of its methods.
The following code was completed starting fromthe the snippets of the article:
class Color
{
// ...
}
class SomeClass
{
string SomeMethod(string s, bool reformulate)
{
OtherClass o = new OtherClass(s);
string result = Frob(o);
// o is elegible for garbage collection here
if (reformulate)
{
Reformulate();
}
return result;
}
string Frob(OtherClass o)
{
string result = FrobColor(o.GetEffectiveColor());
// o is elegible for garbage collection here
return result;
}
string FrobColor(Color color)
{
return color.ToString();
}
void Reformulate()
{
// ...
}
}
class OtherClass
{
public OtherClass(string s)
{
_ = s;
}
OtherClass Parent {get; set;}
Color Color {get; set;}
public Color GetEffectiveColor()
{
Color color = this.Color;
for (OtherClass o = this.Parent; o != null; o = o.Parent)
{
// this is elegible for garbage collection here
color = BlendColors(color, o.Color);
}
return color;
}
static Color BlendColors(Color left, Color right)
{
_ = right;
return left;
}
}
Please note that the garbage collector does scan the stack for references. Thus, for this to work, inlining is required.
It is also worth noting that this
is not a root for the garbage collector, nor the finalizer thread.
And that is without talking about WeakReference
The situation where the finalizer and the dispose methods to be running at the same time is very unlikely.
However, you need to decide if your type is meant to be thread safe. If that is the case, you should worry about two consumer threads calling dispose.
And this pattern is not thread safe:
if (!this.disposed)
{
// ...
this.disposed = true;
}
I would suggest to update and check a disposed
field with an interlocked operation:
if (Interlocked.Exchange(_disposed, 1) == 0)
{
// ...
}
However, dealing with managed resources should just call Dispose
and set to null. You can do that like this:
Interlocked.Exchange(ref _managedResource, null)?.Dispose();
(...) I have problems with mentioning exactly the parallel execution of this.Dispose(true) and this.Dispose(false)
If the finalizer always calls Dispose(false)
, while Dispose
calls Dispose(true)
. You know that the code under the conditional disposing == true
will only run from Dispose
. And thus, it will not be running in parallel from the finalizer.
In addition to that, the execution of GC.SuppressFinalize(this)
prevents finalization. Since there is a reference to this
, the object is not eligible before the call to SuppressFinalize
and, the call prevents the finalization completely.
I will remind you that the order of finalization is not deterministic. During the execution of the finalizer, the reference type fields of the object being finalized could have been finalized already. There are no guarantees about there order. Because of that, you should not be acceding them from the finalizer.
In fact, I would like to encourage to do not have objects that deal with both managed and unmanaged resources at the same time. By virtue of single responsibility principle, wrap any unmanaged resources in managed ones (you deal with them by calling some external API).
And then classes that need them only have to deal with managed resources (and dealing with them is just calling Dispose)
.
Furthermore, if you do not have unmanaged resources, avoid the finalizer.
Upvotes: 1
Reputation: 1064004
1) Here I have a first question, how is this possible? After all, GC CLR destroys the object as correctly noticed when there are no more links to it. But if there are no references, how then can simultaneously still work the method of an object (Dispose()) to which nothing else refers? Yes, there are no links, but the method is not completed and GC CLR will try to delete the method object that still works?
Imagine that you have a method like:
void SomeMethod()
{
var unmanagedPtr = this.MyPointer;
while (/* some long loop */)
{
// lots of code that *just* uses unmanagedPtr
}
}
Now; this
here is arg0
, so does exist in the stack, but the GC
is allowed to look at when locals are read, and arg0
is not read past the first few instructions; so from the perspective of GC
, it can ignore arg0
if the thread is in the while
loop. Now; imagine that somehow the reference to this object only exists in arg0
- perhaps because it was only ever transient on the stack, i.e.
new MyType(...).SomeMethod();
At this point, yes, the object can be collected even though a method is executing on it. In most scenarios, we would never notice any side effect from this, but: finalizers and unmanaged data is a bit of a special case, beause if your finalizer invalidates the unmanagedPtr
that the while
loop is depending on: bad things.
The most appropriate fix here, is probably to just add GC.KeepAlive(this)
to the end of SomeMethod
. Importantly, note that GC.KeepAlive
does literally nothing - it is an opaque, no-op, non-inlineable method, nothing else. All we're actually doing by adding GC.KeepAlive(this)
is adding a read against arg0
, which means that the GC needs to look at arg0
, so it notices that the object is still reachable, and doesn't get collected.
2) Which in the proposed last solution will not allow GC CLR to call the this.Dispose(false) method in its thread in parallel through the destructor in the same way as before while this.Dispose(true) will still be executed previously launched explicitly?
For us to be able to call Dispose()
, we clearly have a reference, so that's good. So we know it was reachable at least until Dispose
, and we're only talking about Dispose(true)
competing with Dispose(false)
. In this scenario, the GC.SuppressFinalize(this)
serves two purposes:
GC.SuppressFinalize(this)
acts the same as GC.KeepAlive
and marks the object as reachable; it can't possibly be collected until that point is reachedUpvotes: 5