代码之家  ›  专栏  ›  技术社区  ›  paercebal

终结器在其对象仍在使用时启动

  •  3
  • paercebal  · 技术社区  · 16 年前

    总结: C/.NET应该是垃圾收集。C有一个析构函数,用于清理资源。当一个对象A被垃圾收集到我试图克隆它的一个变量成员的同一行时会发生什么?显然,在多处理器上,有时垃圾收集器会赢…

    问题

    今天,在一个关于C的培训课程中,老师向我们展示了一些仅在多处理器上运行时才包含bug的代码。

    我总结一下,有时编译器或JIT会在从其调用的方法返回之前,通过调用C类对象的终结器而出错。

    在VisualC++ 2005文档中给出的完整代码将被贴上“答案”,以避免提出非常大的问题,但要点如下:

    以下类具有“hash”属性,该属性将返回内部数组的克隆副本。在is construction中,数组的第一个项的值为2。在析构函数中,其值设置为零。

    要点是:如果您试图获取“example”的“hash”属性,那么在使用对象(因此,不会被垃圾收集/最终确定)时,您将得到一个第一项仍为2的数组的干净副本:

    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);
            }
        }
    }
    

    但没有什么是如此简单… 使用这个类的代码是在线程中工作的,当然,对于测试来说,应用程序是多线程的:

    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();
    }
    

    DoWork静态方法是出现问题的代码:

    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
        }
    }
    

    显然,每执行1000000次任务,垃圾收集器就会发挥其魔力,并尝试回收“ex”,因为函数的剩余代码中不再引用它,而这一次,它比“hash”get方法更快。因此,我们最后得到的是一个零字节数组的克隆,而不是一个正确的克隆(第一项在2)。

    我的猜测是代码中有一个内嵌,它本质上将dowork函数中标记为[1]的行替换为如下内容:

        // 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();
    

    如果我们假设hash2是一个简单的访问器,代码如下:

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

    所以,问题是: 这是应该在C/.NET中以这种方式工作,还是可以将其视为JIT编译器的bug?

    编辑

    有关解释,请参阅ChrisBrumme和ChrisLyons的博客。

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

    每个人的回答都很有趣,但我无法选择一个比另一个更好的答案。所以我给你们所有人一个+1…

    对不起的

    -)

    编辑2

    我无法在linux/ubuntu/mono上重现这个问题,尽管在相同的条件下使用相同的代码(多个相同的可执行文件同时运行,发布模式等)。

    8 回复  |  直到 16 年前
        1
  •  8
  •   to StackOverflow    16 年前

    这只是代码中的一个错误:终结器不应该访问托管对象。

    实现终结器的唯一原因是释放非托管资源。在这种情况下,您应该小心地执行 the standard IDisposable pattern .

    使用此模式,可以实现受保护的方法“Protected Dispose(bool Disposing)”。当从终结器调用此方法时,它将清除非托管资源,但不尝试清除托管资源。

    在您的示例中,您没有任何非托管资源,因此不应实现终结器。

        2
  •  3
  •   Lasse V. Karlsen    16 年前

    你所看到的完全是自然的。

    不保留对拥有字节数组的对象的引用,因此对象(而不是字节数组)实际上是垃圾收集器可以收集的空闲对象。

    垃圾收集器真的有那么强的攻击性。

    因此,如果对对象调用一个方法,该方法返回对内部数据结构的引用,而对象的终结器也会破坏该数据结构,则还需要保持对该对象的实时引用。

    垃圾收集器发现该方法不再使用ex变量,因此它可以,并且正如您所注意到的,在正确的情况下(即时间和需要)垃圾收集它。

    正确的方法是在ex上调用gc.keepalive,因此将这一行代码添加到方法的底部,所有代码都应该是好的:

    GC.KeepAlive(ex);
    

    我通过阅读这本书了解到这种侵略行为 Applied .NET Framework Programming 作者:杰弗里·里克特。

        3
  •  1
  •   Steven A. Lowe    16 年前

    这看起来像工作线程和GC线程之间的竞争条件;为了避免这种情况,我认为有两种选择:

    (1)将if语句更改为使用ex.hash[0]而不是res,这样ex就不能过早地被gc使用,或者

    (2)在调用哈希期间锁定ex

    这是一个非常尖锐的例子——老师的观点是,JIT编译器中可能存在一个只在多核系统上显示的错误,还是这种编码在垃圾收集中可能有微妙的竞争条件?

        4
  •  1
  •   Scott Dorman    16 年前

    我想你看到的是 合理的 由于在多个线程上运行而导致的行为。这就是gc.keepalive()方法的原因,在本例中应该使用该方法来告诉gc该对象仍在使用中,并且它不是清理的候选对象。

    查看“完整代码”响应中的DoWork函数,问题是在这行代码之后:

    byte[] res = ex.Hash;
    

    函数不再引用 前任 对象,因此它可以在此时进行垃圾收集。添加对gc.keepalive的调用可以防止这种情况发生。

        5
  •  1
  •   Jack Bolding    16 年前

    是的,这是一个 issue 那有 come up before .

    更有趣的是,你需要运行释放来实现这一点,你最终会绞尽脑汁“啊,怎么可能是空的?”.

        6
  •  1
  •   paercebal    16 年前

    来自ChrisBrumme博客的有趣评论

    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 {
             ...
          }
       }
    }
    

    因此,我们可以用上面的代码来表示“ac”可能存在多长时间。在其他.work()完成之前,JIT可能会报告引用。它可能将other.work()内联到其他方法中,并报告ac的时间更长。即使在使用后添加了__ac=null;__,JIT也可以自由地将此分配视为死代码并消除它。无论何时JIT停止报告引用,GC可能在一段时间内无法收集它。

    更有趣的是,担心最早收集到的AC数据。如果你和大多数人一样,你会猜测最快获得收藏资格的AC是在Other.work()_S__if_157;子句的右括号处,我在其中添加了评论。事实上,大括号在IL中不存在。它们是您和语言编译器之间的语法契约。 other.work()可以在启动对ac.m()的调用后立即停止报告ac。

        7
  •  1
  •   Palad1    16 年前

    这对于在Do-Work方法中调用终结器是完全正常的,就像在 例如散列调用,clr知道不再需要ex实例…

    现在,如果要使实例保持活动状态,请执行以下操作:

    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) // NOTE
        {
            // Oops... The finalizer of ex was launched before
            // the Hash method/property completed
        }
      GC.KeepAlive(ex); // keep our instance alive in case we need it.. uh.. we don't
    }
    

    gc.keepalive确实…什么都没有:)这是一个空的、不可内联的/jittable方法,其唯一目的是诱使GC认为在此之后将使用该对象。

    警告:如果DoOrk方法是一个托管C++方法,您的示例是完全有效的…你 如果不希望从另一个线程中调用析构函数,则必须手动保持托管实例的活动状态。例如,传递对托管对象的引用,该对象在完成时将删除非托管内存的blob,而该方法使用的是相同的blob。如果不将实例保持活动状态,则在GC和方法线程之间会有一个争用条件。

    最后会流下眼泪。管理堆损坏…

        8
  •  0
  •   paercebal    16 年前

    完整代码

    您将在完整代码下面找到,从VisualC++ 2008 .cs文件复制/粘贴。因为我现在在Linux上,没有任何单编译程序或它的使用知识,所以我现在无法进行测试。不过,几个小时前,我看到了这段代码的工作和它的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();
        }
    }
    

    对于那些感兴趣的人,我可以通过电子邮件发送压缩项目。