user12639686
user12639686

Reputation:

C# IDisposable, Dispose(), lock (this)

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

Answers (2)

Theraot
Theraot

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

Marc Gravell
Marc Gravell

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:

  • the mere existence of 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 reached
  • and once it has been reached, it won't get finalized at all

Upvotes: 5

Related Questions