GC.KeepAlive()当这个被取消注释时,仍然只有一个实例,怎么样?

反对下面的引用线程,即使我注释GC.KeepAlive(),我发现没有区别,它阻止了任何其他实例的创建。 为什么作者明确提到了它的重要路线?

确保只有一个应用程序实例

如果你不这样做,互斥锁将在垃圾收集时被销毁,但这不是一个保证事件立即发生,这就是为什么它仍然可以工作很长时间。

我会用一个静态的自我看到第二个答案。

更新:这个问题是我2013年6月的博客主题 ; 请参阅该文章,了解有关此主题的更多想法。 谢谢你这个好问题!


让我澄清一下这里发生了什么。 让我们忽略它是互斥的事实并考虑一般情况:

class Foo { public Foo() { this.x = whatever; this.y = whatever; SideEffects.Alpha(); // Does not use "this" } ~Foo() { SideEffects.Charlie(); } ... } static class SideEffects { public static void Alpha() {...} public static void Bravo() {...} public static void Charlie() {...} public static void M() { Foo foo = new Foo(); // Allocating Foo has side effect Alpha Bravo(); // Foo's destructor has side effect Charlie } } 

M的作者希望Alpha()Bravo()Charlie()副作用Alpha()顺序发生。 现在,您可能会认为Alpha()必须在Bravo()之前发生,因为Alpha()Bravo()发生在同一个线程上,而C#保证在同一个线程上发生的事情保留了副作用的顺序。 你是对的。

您可能会认为Charlie()必须在Bravo()之后发生,因为Charlie()直到存储在foo的引用被垃圾收集才会发生,并且局部变量foo保持该引用处于活动状态,直到控制离开foo的范围,在调用之后Bravo()这是错的。 允许C#编译器和CLR jit编译器一起工作,以允许将局部变量提前声明为“死”。 请记住,C#仅保证在从一个线程观察时 ,事物以可预测的顺序发生。 垃圾收集器和终结器在自己的线程上运行! 因此,垃圾收集器推断foo (在Bravo()没有使用 – 在调用Bravo()之前已经死了,因此副作用Charlie()可以在Bravo()之前发生 – 或者 Bravo() ,在另一个线程上。

KeepAlive(foo)会阻止编译器进行此优化; 它告诉垃圾收集器foo至少在KeepAlive之前是活着的,因此终结器中的副作用Charlie()保证在副作用Bravo()

现在这是一个引人入胜的问题。 没有keepalive,可以副作用Charlie()发生Alpha()之前吗? 事实certificate,在某些情况下是的! 抖动被允许非常激进; 如果它发现ctor的this将在ctor结束后立即死亡,那么在ctor停止变异之后的那一刻它被允许安排收集。

除非你在Foo()构造函数中执行KeepAlive(this) ,否则允许垃圾收集器在Alpha()运行之前收集它! (当然,没有提供任何其他东西使它保持活着。) 一个对象可以在其构造函数仍然在另一个线程上运行时完成。 这是你需要非常小心地编写具有析构函数的类的另一个原因。 整个对象需要设计为在析构函数突然被另一个线程意外调用时面对强大,因为抖动是激进的。

在您的特定情况下,“Alpha”是取出互斥锁的副作用,“Bravo”是运行整个程序的副作用,而“Charlie”是释放互斥锁的副作用。 如果没有keepalive, 则允许在程序运行之前释放互斥锁,或者更有可能在它运行时释放互斥锁。 它不需要被释放,并且大多数时间它不会被释放。 但它可能是 ,如果抖动决定采取积极措施取出垃圾。


有哪些替代方案?

keepalive是正确的,但我个人不会在原始代码中选择keepalive,这基本上是:

 static void Main() { Mutex m = GetMutex(); Program.Run(); GC.KeepAlive(m); } 

另一种选择是:

 static Mutex m; static void Main() { m = GetMutex(); Program.Run(); } 

允许抖动尽早杀死局部变量,但不允许早期杀死静态变量。 因此,不需要KeepAlive。

更好的方法是利用Mutex是一次性的这一事实:

 static void Main() { using(GetMutex()) Program.Run(); } 

这是一个简短的写作方式:

 static void Main() { Mutex m = GetMutex(); try { Program.Run(); } finally { ((IDisposable)m).Dispose(); } } 

而现在垃圾收集器无法提前清理互斥锁,因为它需要控制离开Run 进行处理。 (如果抖动可以certificate处理互斥锁什么也不做,那么就可以提前清理它,但在这种情况下,配置互斥锁会产生影响。)

没有多少测试可以certificateGC.KeepAlive是多余的,但只有一个(失败的)测试可以certificate它是必要的。

换句话说,如果省略GC.KeepAlive ,则代码可能无法正常工作; 它不能保证立即破裂。 它可能无法正常工作,因为mutex是一个超出范围的局部变量; 这使它有资格进行垃圾收集。 如果 GC决定收集该对象,则将释放互斥锁(并且您将能够启动新实例)。