Finalizer launched while its object was still bein

2019-04-11 06:33发布

Summary: C#/.NET is supposed to be garbage collected. C# has a destructor, used to clean resources. What happen when an object A is garbage collected the same line I try to clone one of its variable members? Apparently, on multiprocessors, sometimes, the garbage collector wins...

The problem

Today, on a training session on C#, the teacher showed us some code which contained a bug only when run on multiprocessors.

I'll summarize to say that sometimes, the compiler or the JIT screws up by calling the finalizer of a C# class object before returning from its called method.

The full code, given in Visual C++ 2005 documentation, will be posted as an "answer" to avoid making a very very large questions, but the essential are below:

The following class has a "Hash" property which will return a cloned copy of an internal array. At is construction, the first item of the array has a value of 2. In the destructor, its value is set to zero.

The point is: If you try to get the "Hash" property of "Example", you'll get a clean copy of the array, whose first item is still 2, as the object is being used (and as such, not being garbage collected/finalized):

public class Example
{
    private int nValue;
    public int N { get { return nValue; } }

    // The Hash property is slower because it clones an array. When
    // KeepAlive is not used, the finalizer sometimes runs before 
    // the Hash property value is read.

    private byte[] hashValue;
    public byte[] Hash { get { return (byte[])hashValue.Clone(); } }

    public Example()
    {
        nValue = 2;
        hashValue = new byte[20];
        hashValue[0] = 2;
    }

    ~Example()
    {
        nValue = 0;

        if (hashValue != null)
        {
            Array.Clear(hashValue, 0, hashValue.Length);
        }
    }
}

But nothing is so simple... The code using this class is wokring inside a thread, and of course, for the test, the app is heavily multithreaded:

public static void Main(string[] args)
{
    Thread t = new Thread(new ThreadStart(ThreadProc));
    t.Start();
    t.Join();
}

private static void ThreadProc()
{
    // running is a boolean which is always true until
    // the user press ENTER
    while (running) DoWork();
}

The DoWork static method is the code where the problem happens:

private static void DoWork()
{
    Example ex = new Example();

    byte[] res = ex.Hash; // [1]

    // If the finalizer runs before the call to the Hash 
    // property completes, the hashValue array might be
    // cleared before the property value is read. The 
    // following test detects that.

    if (res[0] != 2)
    {
        // Oops... The finalizer of ex was launched before
        // the Hash method/property completed
    }
}

Once every 1,000,000 excutions of DoWork, apparently, the Garbage Collector does its magic, and tries to reclaim "ex", as it is not anymore referenced in the remaning code of the function, and this time, it is faster than the "Hash" get method. So what we have in the end is a clone of a zero-ed byte array, instead of having the right one (with the 1st item at 2).

My guess is that there is inlining of the code, which essentially replaces the line marked [1] in the DoWork function by something like:

    // Supposed inlined processing
    byte[] res2 = ex.Hash2;
    // note that after this line, "ex" could be garbage collected,
    // but not res2
    byte[] res = (byte[])res2.Clone();

If we supposed Hash2 is a simple accessor coded like:

// Hash2 code:
public byte[] Hash2 { get { return (byte[])hashValue; } }

So, the question is: Is this supposed to work that way in C#/.NET, or could this be considered as a bug of either the compiler of the JIT?

edit

See Chris Brumme's and Chris Lyons' blogs for an explanation.

http://blogs.msdn.com/cbrumme/archive/2003/04/19/51365.aspx
http://blogs.msdn.com/clyon/archive/2004/09/21/232445.aspx

Everyone's answer was interesting, but I couldn't choose one better than the other. So I gave you all a +1...

Sorry

:-)

Edit 2

I was unable to reproduce the problem on Linux/Ubuntu/Mono, despite using the same code on the same conditions (multiple same executable running simultaneously, release mode, etc.)

8条回答
一夜七次
2楼-- · 2019-04-11 06:50

Yes, this is an issue that has come up before.

Its even more fun in that you need to run release for this to happen and you end up stratching your head going 'huh, how can that be null?'.

查看更多
Juvenile、少年°
3楼-- · 2019-04-11 06:50

Interesting comment from Chris Brumme's blog

http://blogs.msdn.com/cbrumme/archive/2003/04/19/51365.aspx

class C {<br>
   IntPtr _handle;
   Static void OperateOnHandle(IntPtr h) { ... }
   void m() {
      OperateOnHandle(_handle);
      ...
   }
   ...
}

class Other {
   void work() {
      if (something) {
         C aC = new C();
         aC.m();
         ...  // most guess here
      } else {
         ...
      }
   }
}

So we can’t say how long ‘aC’ might live in the above code. The JIT might report the reference until Other.work() completes. It might inline Other.work() into some other method, and report aC even longer. Even if you add “aC = null;” after your usage of it, the JIT is free to consider this assignment to be dead code and eliminate it. Regardless of when the JIT stops reporting the reference, the GC might not get around to collecting it for some time.

It’s more interesting to worry about the earliest point that aC could be collected. If you are like most people, you’ll guess that the soonest aC becomes eligible for collection is at the closing brace of Other.work()’s “if” clause, where I’ve added the comment. In fact, braces don’t exist in the IL. They are a syntactic contract between you and your language compiler. Other.work() is free to stop reporting aC as soon as it has initiated the call to aC.m().

查看更多
Animai°情兽
4楼-- · 2019-04-11 06:54

this looks like a race condition between your work thread and the GC thread(s); to avoid it, i think there are two options:

(1) change your if statement to use ex.Hash[0] instead of res, so that ex cannot be GC'd prematurely, or

(2) lock ex for the duration of the call to Hash

that's a pretty spiffy example - was the teacher's point that there may be a bug in the JIT compiler that only manifests on multicore systems, or that this kind of coding can have subtle race conditions with garbage collection?

查看更多
甜甜的少女心
5楼-- · 2019-04-11 06:55

I think what you are seeing is reasonable behavior due to the fact that things are running on multiple threads. This is the reason for the GC.KeepAlive() method, which should be used in this case to tell the GC that the object is still being used and that it isn't a candidate for cleanup.

Looking at the DoWork function in your "full code" response, the problem is that immediately after this line of code:

byte[] res = ex.Hash;

the function no longer makes any references to the ex object, so it becomes eligible for garbage collection at that point. Adding the call to GC.KeepAlive would prevent this from happening.

查看更多
Animai°情兽
6楼-- · 2019-04-11 07:00

The Full Code

You'll find below the full code, copy/pasted from a Visual C++ 2008 .cs file. As I'm now on Linux, and without any Mono compiler or knowledge about its use, there's no way I can do tests now. Still, a couple of hours ago, I saw this code work and its bug:

using System;
using System.Threading;

public class Example
{
    private int nValue;
    public int N { get { return nValue; } }

    // The Hash property is slower because it clones an array. When
    // KeepAlive is not used, the finalizer sometimes runs before 
    // the Hash property value is read.

    private byte[] hashValue;
    public byte[] Hash { get { return (byte[])hashValue.Clone(); } }
    public byte[] Hash2 { get { return (byte[])hashValue; } }

    public int returnNothing() { return 25; }

    public Example()
    {
        nValue = 2;
        hashValue = new byte[20];
        hashValue[0] = 2;
    }

    ~Example()
    {
        nValue = 0;

        if (hashValue != null)
        {
            Array.Clear(hashValue, 0, hashValue.Length);
        }
    }
}

public class Test
{
    private static int totalCount = 0;
    private static int finalizerFirstCount = 0;

    // This variable controls the thread that runs the demo.
    private static bool running = true;

    // In order to demonstrate the finalizer running first, the
    // DoWork method must create an Example object and invoke its
    // Hash property. If there are no other calls to members of
    // the Example object in DoWork, garbage collection reclaims
    // the Example object aggressively. Sometimes this means that
    // the finalizer runs before the call to the Hash property
    // completes. 

    private static void DoWork()
    {
        totalCount++;

        // Create an Example object and save the value of the 
        // Hash property. There are no more calls to members of 
        // the object in the DoWork method, so it is available
        // for aggressive garbage collection.

        Example ex = new Example();

        // Normal processing
        byte[] res = ex.Hash;

        // Supposed inlined processing
        //byte[] res2 = ex.Hash2;
        //byte[] res = (byte[])res2.Clone();

        // successful try to keep reference alive
        //ex.returnNothing();

        // Failed try to keep reference alive
        //ex = null;

        // If the finalizer runs before the call to the Hash 
        // property completes, the hashValue array might be
        // cleared before the property value is read. The 
        // following test detects that.

        if (res[0] != 2)
        {
            finalizerFirstCount++;
            Console.WriteLine("The finalizer ran first at {0} iterations.", totalCount);
        }

        //GC.KeepAlive(ex);
    }

    public static void Main(string[] args)
    {
        Console.WriteLine("Test:");

        // Create a thread to run the test.
        Thread t = new Thread(new ThreadStart(ThreadProc));
        t.Start();

        // The thread runs until Enter is pressed.
        Console.WriteLine("Press Enter to stop the program.");
        Console.ReadLine();

        running = false;

        // Wait for the thread to end.
        t.Join();

        Console.WriteLine("{0} iterations total; the finalizer ran first {1} times.", totalCount, finalizerFirstCount);
    }

    private static void ThreadProc()
    {
        while (running) DoWork();
    }
}

For those interested, I can send the zipped project through email.

查看更多
Viruses.
7楼-- · 2019-04-11 07:01

It's simply a bug in your code: finalizers should not be accessing managed objects.

The only reason to implement a finalizer is to release unmanaged resources. And in this case, you should carefully implement the standard IDisposable pattern.

With this pattern, you implement a protected method "protected Dispose(bool disposing)". When this method is called from the finalizer, it cleans up unmanaged resources, but does not attempt to clean up managed resources.

In your example, you don't have any unmanaged resources, so should not be implementing a finalizer.

查看更多
登录 后发表回答