Finalization is the crazy wildcard in garbage collection. It operates "behind the GC", running after the GC has declared an object dead. Think about it: Finalizers run on objects that have no active references. How can this be a reference to an object that has no references? That's just crazy-talk!
Finalizers are a Ouija board, permitting dead objects to operate "from beyond the grave" and affect live objects. As a result, when finalizers are involved, there is a lot of creepy spooky juju going on, and you need to tread very carefully, or your soul will become cursed.
Let's step back and look at a different problem first. Consider this class which doesn't do anything interesting but works well enough for demonstration purposes:
class Sample1 {
private StreamReader sr;
public Sample1(string file) : sr(new StreamReader(file)) { }
public void Close() { sr.Close(); }
public string NextLine() { return sr.ReadLine(); }
}
What happens if one thread calls Sample1.NextLine() and another thread calls Sample1.Close()? If the NextLine() call wins the race, then you have a stream closed while it is in the middle of its ReadLine method. Probably not good. If the Close() call wins the race, then when the NextLine() call is made, you end up reading from a closed stream. Definitely not good. Finally, if the NextLine() call runs to completion before the Close(), then the line is successfully read before the stream is closed.
Having this race condition is clearly an unwanted state of affairs since the result is unpredictable.
Now let's change the Close() method to a finalizer.
class Sample2 {
private StreamReader sr;
public Sample2(string file) : sr(new StreamReader(file)) { }
~Sample2() { sr.Close(); }
public string NextLine() { return sr.ReadLine(); }
}
Remember that we learned that an object becomes eligible for garbage collection when there are no active references to it, and that it can happen even while a method on the object is still active. Consider this function:
string FirstLine(string fileName) {
Sample2 s = new Sample2(fileName);
return s.NextLine();
}
We learned that the Sample2 object becomes eligible for collection during the execution of NextLine(). Suppose that the garbage collector runs and collects the object while NextLine is still running. This could happen if ReadLine takes a long time, say, because the hard drive needs to spin up or there is a network hiccup; or it could happen just because it's not your lucky day and the garbage collector ran at just the wrong moment. Since this object has a finalizer, the finalizer runs before the memory is discarded, and the finalizer closes the StreamReader.
Boom, we just hit the race condition we considered when we looked at Sample1: The stream was closed while it was being read from. The garbage collector is a rogue thread that closes the stream at a bad time. The problem occurs because the garbage collector doesn't know that the finalizer is going to make changes to other objects.
Read more: The Old New Thing
QR:
0 comments:
Post a Comment