Can I access reference type instance fields/properties safely within a finalizer?
Asked Answered
D

2

10

I always thought that the answer to this was no, but I cannot find any source stating this. In my class below, can I access (managed) fields/properties of an instance of C in the finalizer, i.e. in ReleaseUnmanaged()? What restrictions, if any, are there? Will GC or finalization ever have set these members to null?

The only thing I can find is that stuff on the finalizer queue can be finalized in any order. So in that case, since the recommendation is that types should allow users to call Dispose() more than once, why does the recommended pattern bother with a disposing boolean? What bad things might happen if my finalizer called Dispose(true) instead of Dispose(false)?

public class C : IDisposable
{
    private void ReleaseUnmanaged() { }

    private void ReleaseOtherDisposables() { }

    protected virtual void Dispose(bool disposing)
    {
        ReleaseUnmanaged();
        if (disposing)
        {   
            ReleaseOtherDisposables();
        }
    }

    ~ C()
    {
        Dispose(false);
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
}
Downrange answered 23/11, 2013 at 19:56 Comment(1)
As noted here, avoid writing finalizers for classes that don't own unmanaged resources (and even then, try to use SafeHandle instead).Footing
B
3

The GC will never collect any object if there is any way via which any code could ever get a reference to it. If the only references that exist to an object are weak references, the GC will invalidate them so that there's no way any code could ever get a reference to that object, whereupon it will then be able to collect it.

If an object has an active finalizer, then if the GC would collect it (but for the existence of the finalizer), the GC will instead add it to a queue of objects whose finalizers should be run ASAP and, having done so, de-activate it. The reference within the queue will prevent the GC from collecting the object until the finalizer has run; once the finalizer finishes, if no other references exist to the object and it hasn't re-registered its finalizer, it will cease to exist.

The biggest problems with finalizers accessing outside objects are:

  • A finalizer will be run in a threading context which is unrelated to any threading context where the object was used, but should not perform any operations which cannot be guaranteed to complete quickly. This often creates contradictory requirement that code use locking when accessing other objects, but that code not block while waiting on locks.

  • If a finalizer has a reference to another object which also has a finalizer, there's no guarantee as to which will run first.

Both of these factors severely limit the ability of objects with finalizers tosafely access outside objects they don't own. Further, because of the way finalization is implemented, it's possible for the system to decide to run a finalizer when no strong references exist, but for external strong references to be created and used before the finalizer runs; the object with the finalizer has no say in whether that happens.

Beera answered 23/11, 2013 at 21:19 Comment(3)
If I understand correctly, the main issue is not that I can't access the objects referenced by my fields/properties, but that I don't have the usual methods of ensuring thread safety? In a simplified case where I somehow know that no threads will be accessing the "owned" objects at the same time as my finalizer, there are no issues?Downrange
@Rob: I would say the two problems mentioned above are the main issues, but I wouldn't characterize either of them individually as the main issue. They both conspire to make life difficult. For example, suppose one has a class which encapsulates a file, and another which encapsulates a logging stream (which in turn encapsulates the file). The logging stream has an option to set checkpoints, declare certain records "tentative", and erase all tentative records since the last checkpoint (the idea being that one would "tentatively" log information that would only be of interest in case...Beera
...something fails, and then abandon such information when an operation succeeds). For efficiency, such a logging class will generally have to buffer information internally unless or until it knows it will actually need to be written. If the logger is abandoned, it should write out all the information it has before the file it encapsulates is closed, but to make that work the finalizable object would have to store some a reference to the file in some kind of static location, to prevent it from being finalized before the logger.Beera
A
4

In general case - yes, you can. If a class has nonempty finalizer, first time GC would collect instance of this class, it calls finalizer instead (only if you didn't call GC.SuppressFinalize on it earlier). Object instance seen from finalizer looks just like it did last time you touched it. You can even create new (direct or indirect) link from root to your instance and thus resurrect it.

Even if you hold unmanaged pointer to unpinned object and inspect raw memory contents, you shouldn't be able to see partially-deallocated object, because .NET uses copying GC. If an instance is alive during collection, it is either promoted to next generation or moved to completely new memory block along with other instances. If it is not reachable it is either left where it was or the whole heap is released and returned to OS. Keep in mind, however, that finalizers can and will be called on instances of objects that failed to construct (i.e. when was an exception thrown during object construction).

Edit: As for Dispose(true) vs Dispose(false) in well-written classes there shouldn't be much of a difference in the long run. If your finalizer called Dispose(true), it would only remove links from your object to other objects, but since your object is already nonreachable, releasing other instances referenced by your object won't matter their reachability.

For more details on .NET GC implementation details, I recommend C# 5.0 in a Nutshell by Joseph and Ben Albahari.

Albrecht answered 23/11, 2013 at 20:55 Comment(0)
B
3

The GC will never collect any object if there is any way via which any code could ever get a reference to it. If the only references that exist to an object are weak references, the GC will invalidate them so that there's no way any code could ever get a reference to that object, whereupon it will then be able to collect it.

If an object has an active finalizer, then if the GC would collect it (but for the existence of the finalizer), the GC will instead add it to a queue of objects whose finalizers should be run ASAP and, having done so, de-activate it. The reference within the queue will prevent the GC from collecting the object until the finalizer has run; once the finalizer finishes, if no other references exist to the object and it hasn't re-registered its finalizer, it will cease to exist.

The biggest problems with finalizers accessing outside objects are:

  • A finalizer will be run in a threading context which is unrelated to any threading context where the object was used, but should not perform any operations which cannot be guaranteed to complete quickly. This often creates contradictory requirement that code use locking when accessing other objects, but that code not block while waiting on locks.

  • If a finalizer has a reference to another object which also has a finalizer, there's no guarantee as to which will run first.

Both of these factors severely limit the ability of objects with finalizers tosafely access outside objects they don't own. Further, because of the way finalization is implemented, it's possible for the system to decide to run a finalizer when no strong references exist, but for external strong references to be created and used before the finalizer runs; the object with the finalizer has no say in whether that happens.

Beera answered 23/11, 2013 at 21:19 Comment(3)
If I understand correctly, the main issue is not that I can't access the objects referenced by my fields/properties, but that I don't have the usual methods of ensuring thread safety? In a simplified case where I somehow know that no threads will be accessing the "owned" objects at the same time as my finalizer, there are no issues?Downrange
@Rob: I would say the two problems mentioned above are the main issues, but I wouldn't characterize either of them individually as the main issue. They both conspire to make life difficult. For example, suppose one has a class which encapsulates a file, and another which encapsulates a logging stream (which in turn encapsulates the file). The logging stream has an option to set checkpoints, declare certain records "tentative", and erase all tentative records since the last checkpoint (the idea being that one would "tentatively" log information that would only be of interest in case...Beera
...something fails, and then abandon such information when an operation succeeds). For efficiency, such a logging class will generally have to buffer information internally unless or until it knows it will actually need to be written. If the logger is abandoned, it should write out all the information it has before the file it encapsulates is closed, but to make that work the finalizable object would have to store some a reference to the file in some kind of static location, to prevent it from being finalized before the logger.Beera

© 2022 - 2024 — McMap. All rights reserved.