C# 如果一个易失性引用在加载该引用的线程和调用该引用上的函数之间发生了变化,那么旧对象可以被垃圾收集吗?

C# 如果一个易失性引用在加载该引用的线程和调用该引用上的函数之间发生了变化,那么旧对象可以被垃圾收集吗?,c#,concurrency,garbage-collection,C#,Concurrency,Garbage Collection,我有两个线程执行下面的代码: static volatile Something foo; void update() { newFoo = new Something(); foo = newFoo; } void invoke() { foo.Bar(); } 线程A执行update,线程B执行invoke。这两个线程的计时方式是invoke加载foo的地址,update覆盖foo,然后在调用Bar之前进行垃圾收集 垃圾收集是否可能会收集foo引用的旧对象,从而

我有两个线程执行下面的代码:

static volatile Something foo;

void update() {
    newFoo = new Something();
    foo = newFoo;
}

void invoke() {
    foo.Bar();
}
线程A执行
update
,线程B执行
invoke
。这两个线程的计时方式是
invoke
加载
foo
的地址,
update
覆盖
foo
,然后在调用
Bar
之前进行垃圾收集

垃圾收集是否可能会收集
foo
引用的旧对象,从而导致在已收集的某些内存上调用
Bar

请注意,这个问题主要是出于好奇。我也希望有一个更好的头衔

这两个线程具有定时功能,以便invoke加载foo的地址,update覆盖foo

因为您的静态字段被标记为volatile,所以运行时保证对给定字段的任何更改都将立即更新,并且使用该字段的任何线程都将具有最外面的更新值。因此,目前不可能出现这种情况

如果该字段不是易失性的,则
foo
将持有对以前值的引用,使得GC无法收集它,因此
Bar()
将在旧引用上调用,而不是在“可能收集的某些内存”上调用。NET中的内存管理非常适合处理此类情况,因此不会执行任意不安全的内存地址。

不,不会在“收集的内存”上调用它。对旧对象或新创建的对象调用
Bar()
方法。这取决于它们中的哪一个首先加载到堆栈上。(我不认为堆栈是垃圾收集的)

从反编译代码中可以清楚地看出:

.method private hidebysig static 
    void update () cil managed 
{
    // Method begins at RVA 0x2170
    // Code size 16 (0x10)
    .maxstack 1
    .locals init (
        [0] class ConsoleApplication1.Something newFoo
    )

    IL_0000: nop
    IL_0001: newobj instance void ConsoleApplication1.Something::.ctor()
    IL_0006: stloc.0
    IL_0007: ldloc.0
    IL_0008: volatile.
    IL_000a: stsfld class ConsoleApplication1.Something modreq([mscorlib]System.Runtime.CompilerServices.IsVolatile)  ConsoleApplication1.Program::foo
    IL_000f: ret
} // end of method Program::update


.method private hidebysig static 
    void invoke () cil managed 
{
    // Method begins at RVA 0x218c
    // Code size 15 (0xf)
    .maxstack 8

    IL_0000: nop
    IL_0001: volatile.
    IL_0003: ldsfld class ConsoleApplication1.Something modreq([mscorlib]System.Runtime.CompilerServices.IsVolatile)  ConsoleApplication1.Program::foo
    IL_0008: callvirt instance class ConsoleApplication1.Something ConsoleApplication1.Something::Bar()
    IL_000d: pop
    IL_000e: ret
} // end of method Program::invoke
stsfld
-将静态字段的值替换为计算堆栈中的值

ldsfld
-将静态字段的值推送到计算堆栈上


callvirt
-调用对象上的后期绑定方法,将返回值推送到计算堆栈上。

这有两件事:

  • 当GC进行收集时,它从所谓的“根”开始确定是否引用了对象实例。根可以是存储在堆栈上的局部变量,甚至可以是保存对象引用的寄存器。当您的代码调用Bar()时,代码可能会将实例地址加载到寄存器中(几年前是ECX,现在我不确定)。它将作为“this”传递给方法。如果发生垃圾收集,ECX将被视为根,实例将不会被标记为垃圾
  • GC不会在任何随机时间发生。在GC发生之前,线程在指定的“安全点”停止,因此程序将处于良好且一致的垃圾收集状态。这有助于避免您描述的情况

垃圾收集器将暂停所有正在运行的线程的状态足够长的时间,以解决由此产生的任何内存访问周围的任何争用情况。无论静态变量
foo
是否易变,垃圾收集器都将知道调用
Bar
时可能调用的任何对象的标识,并将确保只要存在任何正常或“隐藏”的执行路径,任何此类对象都将继续存在可以访问其字段,通过该字段可以对其执行
KeepAlive
调用,或者通过该字段可以将其与另一个引用进行比较


在某些情况下,当对象存在可观察的引用时,系统可能会在对象上调用
Finalize
,但系统保持绝对不变,即GC知道任何执行路径可以以上述方式使用的所有引用;只要存在这样的引用,就保证对象存在

这两个线程具有定时功能,以便invoke加载foo的地址

这就提供了你的答案。当
foo
的旧值位于堆栈上时(准备调用.Bar()),它被视为根引用。它将成为(已经是)条形图中的
引用,并且只要不再需要,就可以收集实例。这可能是在执行Bar()期间发生的


内存安全在这里永远不会有风险

“在某些情况下,系统可能会调用Finalize”-您确定吗?这是哪种系统?@HenkHolterman:在.NET中有很多方法可以实现。定义一个类,该类包含对另一个对象的引用,并具有一个终结器,用于检查状态位置是否为null,如果为null,则将其引用复制到该位置,然后等待750ms左右。在使用终结器创建某个其他类的实例之前和之后创建该类的实例,然后放弃对其中一个实例的所有引用,调用GC.Collect,然后等待静态引用变为非空。@HenkHolterman:在确定的对象的终结器方法有机会运行之前,静态引用很可能会变为非空,时间大约为750ms。请注意,邪恶终结器骗子类不需要使用反射或执行在有限信任环境中不允许的任何操作。在可以回收资源的系统中,我建议安全意识代码应该使用长弱引用的静态列表,以确保在回收任何安全关键资源(例如文件句柄)之前,对具有该资源的对象的所有引用……都完全无效(面向公共的对象不应该有终结器,而是应该拥有对私有对象的引用,而私有对象应该拥有对标识公共对象的长弱引用的引用,而对弱引用的静态引用应该存在,以确保