与下面的参考线程相反,即使我评论 GC.KeepAlive() 也没有发现任何区别,它会阻止任何其他实例的创建。为什么作者明确提到了它的重要线?
3 回答
更新:这个问题是我 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()
必须在之前发生,Bravo()
因为Alpha()
和Bravo()
发生在同一个线程上,而 C# 保证在同一个线程上发生的事情会保留副作用的顺序。你是对的。
您可能会认为这Charlie()
必须在之后发生,Bravo()
因为Charlie()
直到存储在其中的引用foo
被垃圾收集后才会发生,并且局部变量foo
使该引用保持活动状态,直到控制离开 , 的范围foo
后调用Bravo()
. 这是错误的。允许 C# 编译器和 CLR jit 编译器一起工作,以允许提前声明局部变量“死”。请记住,C# 仅保证从一个线程观察时事情以可预测的顺序发生。垃圾收集器和终结器在它们自己的线程上运行!因此,垃圾收集器foo
在Bravo()
调用Bravo()
因此副作用可能在另一个线程Charlie()
之前Bravo()
或期间发生。 Bravo()
AKeepAlive(foo)
会阻止编译器进行这种优化;它告诉垃圾收集器foo
至少在 之前是活动的KeepAlive
,因此Charlie()
终结器中的副作用保证在副作用之后出现Bravo()
。
现在这是一个有趣的问题。如果没有keepalive,之前Charlie()
会发生副作用吗?事实证明,在某些情况下是的!允许抖动非常激进;如果它发现ctor 的 ctor 将在 ctor 结束后立即死亡,那么它可以在 ctor 停止改变 的字段后安排收集。Alpha()
this
this
this
除非您KeepAlive(this)
在Foo()
构造函数中执行 a,否则垃圾收集器可以在运行this
前进行收集Alpha()
!(当然,前提是没有其他东西可以让它保持活动状态。)一个对象可能在其构造函数仍在另一个线程上运行时完成。 这也是为什么您需要非常小心地编写具有析构函数的类的另一个原因。整个对象需要被设计成在面对析构函数突然被另一个线程意外调用时是健壮的,因为抖动是激进的。
在您的具体情况下,“Alpha”是取出互斥锁的副作用,“Bravo”是运行整个程序的副作用,“查理”是释放互斥锁的副作用。如果没有 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
释放。(如果抖动可以证明释放互斥锁没有任何作用,则允许提前清理它,但在这种情况下,释放互斥锁会产生效果。)
如果您不这样做,互斥锁将在垃圾收集时被销毁,但这不能保证事件立即发生,这就是它仍然可以长时间工作的原因。
我会使用我自己的静态来查看第二个答案。
没有多少测试可以证明这GC.KeepAlive
是多余的,但只有一个(失败的)测试可以证明它是必要的。
换句话说,如果GC.KeepAlive
省略,则代码可能无法正常工作;不能保证立即中断。它可能无法正常工作,因为mutex
它是一个超出范围的局部变量;这使它有资格进行垃圾收集。如果GC 决定收集该对象,则互斥锁将被释放(并且您将能够启动一个新实例)。