如果抛出异常,"fixed"是否会得到正确清理?

4
我会假设 fixed 的实现方式类似于 using/try..finally,如果块提前终止(通过返回或抛出异常),指针将被正确清理("unfixed"),以便 GC 再次工作。

然而,在 fixed documentation 中没有看到这样的保证,因此我想知道是否有某种官方保证,或者是否应该在每个 fixed 块中引入一个 try..catch

unsafe void FooUnsafe()
{
    var str = "Foo";
    try
    {
        fixed (char* pStr = str)
        {
            Console.WriteLine("First Char: " + (*pStr));
            throw new Exception("Test");
        }
    }
    catch (Exception ex) {
        Console.WriteLine($"Exception when working with {str}: {ex.Message}");      
    }
}

指针不能比其所指向的资源存在更长的生命周期,因为C#会保护它不会因为悬空指针而发生。因此,如果您指向一个局部变量,那么一旦该变量超出范围,它就一定会被处理。在这种情况下,一旦FooUnsafe返回,指针也将失效。 - FCin
我认为文档(和规范)非常清晰。规范指出对象在“固定语句的持续时间内”被固定。因此,无论是自然地离开固定语句还是作为异常的结果,控制流离开固定语句时,变量都会被取消固定。 - Evk
3个回答

3

从文档中得知:

在语句中的代码执行完成后,任何已固定的变量都将被取消固定并且可以被垃圾回收机制回收。

fixed 语句(C# 参考)


2

它基于作用域。

fixed块内,在句柄表中,对象将被“固定”,GC不会随意重新定位变量。

当抛出异常时,您将退出固定范围,GC将不考虑该内存位置被固定。

我不知道内部实现,但GC可能会检查某个线程的执行点,并根据此确定是否允许重新定位(即基于是否在固定块内)。

您不需要将其放在try/catch/finally块中。


1
GC不会考虑该内存位置是否被固定。有源代码吗?我认为这就是整个问题,无论GC是否会将其固定。 - FCin
是的。我在IL中看到似乎有一个finally块(至少有一个endfinally指令,不确定是否相同),但如果可能的话,我不想依赖于观察到的而非记录下来的功能。 - Michael Stum

2
作为FCin所评论的,
指针不能比其所指向的资源存在更长的生命周期,因为C#保护它不会出现悬挂指针的情况。因此,如果您指向一个局部变量,那么一旦该变量超出作用域,它就一定会被释放。在这种情况下,一旦FooUnsafe返回,指针也会被释放。
还要注意JuanR的评论,
fixed语句(C#参考)执行完语句中的代码后,任何固定的变量都将解除固定并可能面临垃圾回收。
然而,让我们通过一个简单的例子和一些互联网信息片段来证明它。
private static unsafe void Main()
{
   Console.WriteLine($"Total Memory: {GC.GetTotalMemory(false)}");

   var arr = new int[100000];

   Console.WriteLine($"Total Memory after new : {GC.GetTotalMemory(false)}");

   try
   {

      fixed (int* p = arr)
      {
         *p = 1;
         throw new Exception("rah");
      }

   }
   catch 
   {
   }

   Console.WriteLine($"Generation: {GC.GetGeneration(arr)}, Total Memory: {GC.GetTotalMemory(false)}");

   arr = null;
   GC.Collect();
   GC.WaitForPendingFinalizers();
   Console.WriteLine("Total Memory: {0}", GC.GetTotalMemory(false));
   Console.Read();
}

结果

Total Memory: 29948
Total Memory after new: 438172
Generation: 2, Total Memory: 438172
Total Memory: 29824

您会在IL中注意到finallyldnull
.try
{

   // [23 14 - 23 26]
   IL_0043: ldloc.0      // arr
   IL_0044: dup          
   IL_0045: stloc.2      // V_2
   IL_0046: brfalse.s    IL_004d
   IL_0048: ldloc.2      // V_2
   IL_0049: ldlen        
   IL_004a: conv.i4      
   IL_004b: brtrue.s     IL_0052
   IL_004d: ldc.i4.0     
   IL_004e: conv.u       
   IL_004f: stloc.1      // p
   IL_0050: br.s         IL_005b
   IL_0052: ldloc.2      // V_2
   IL_0053: ldc.i4.0     
   IL_0054: ldelema      [mscorlib]System.Int32
   IL_0059: conv.u       
   IL_005a: stloc.1      // p

   ...

} // end of .try
finally
{

   IL_006a: ldnull       
   IL_006b: stloc.2      // V_2
   IL_006c: endfinally   
} // end of finally

有趣的是,在某些情况下编译器会将finally优化掉,因此您不一定总能看到它。

Roslyn源代码中的LocalRewriter_FixedStatement.cs

// In principle, the cleanup code (i.e. nulling out the pinned variables) is always
// in a finally block.  However, we can optimize finally away (keeping the cleanup
// code) in cases where both of the following are true:
//   1) there are no branches out of the fixed statement; and
//   2) the fixed statement is not in a try block (syntactic or synthesized).
if (IsInTryBlock(node) || HasGotoOut(rewrittenBody))
{

即使它存在于像这样的方法中。
private static unsafe void test(int[] arr)
{
   fixed (int* p = arr)
   {
      *p = 1;
   }
}

您会注意到

.method private hidebysig static void 
   test(
   int32[] arr
   ) cil managed 
{
   .maxstack 2
   .locals init (
   [0] int32* p,
   [1] int32[] pinned V_1
   )

   ...

   IL_001e: ldnull       
   IL_001f: stloc.1      // V_1

   // [54 7 - 54 8]
   IL_0020: ret          

} // end of method MyGCCollectClass::test

一些背景信息

标准 ECMA-335 Common Language Infrastructure (CLI)

II.7.1.2 pinned 仅当签名描述局部变量(§II.15.4.1.3)时,才应在固定的签名编码中出现。当具有固定局部变量的方法正在执行时,VES不会将局部变量引用的对象重新定位。也就是说,如果CLI的实现使用移动对象的垃圾回收器,则垃圾回收器不会移动由活动固定局部变量引用的对象。

【原理:如果使用非托管指针对托管对象进行解引用,则这些对象应该被固定。例如,当将托管对象传递给设计用于操作非托管数据的方法时,就会发生这种情况。结束原理】

VES = Virtual Execution System CLI = Common Language Infrastructure CTS = Common Type System

最后,除了 JITerCLR,大多数固定的工作都是由 GC 完成的

实际上,GC必须离开并在整个方法的生命周期内保留 固定的局部变量。通常,GC关心哪些对象是活动的或死亡的,以便知道它必须清理什么。但对于固定的对象,它必须进一步进行,不仅不能清理对象,还不能移动它。通常,在紧凑阶段期间,GC喜欢在对象周围重新定位对象,以使内存分配变得便宜,但固定会阻止这种情况发生,因为对象通过指针访问,因此其内存地址必须保持不变。

显然,您最关心的问题是碎片问题,并且您担心 GC 无法清理它。

enter image description here

然而,正如示例所示(您可以自己尝试),一旦 ary 超出范围并且 fixed 完成,GC 最终将完全释放它。


注意:我不是可靠的来源,并且找不到 官方确认,但我认为我找到的这些信息片段可能仍然很有趣


感谢深入的研究,包括 Roslyn 源代码。我认为这只是确认唯一明智的设计选择,即固定指针在异常处理中也会被清理。 - Michael Stum

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接