如何在最终确定静态变量之前得到通知
我什么时候可以清理存储在C#中的静态变量中的对象?
我有一个懒惰的初始化静态变量:
public class Sqm { private static Lazy _default = new Lazy(); public static Sqm Default { get { return _default.Value; } } }
注意 :我刚刚将Foo
改为static
类。 如果Foo
是静态的,它不会以任何方式改变问题。 但有些人确信,如果不先构建一个Foo
实例,就不可能构造一个Sqm
实例。 即使我确实创建了一个Foo
对象; 即使我创建了100个,它也无法解决问题(何时“清理”静态成员)。
样品用法
Foo.Default.TimerStart("SaveQuestion"); //...snip... Foo.Default.TimerStop("SaveQuestion");
现在,我的Sqm
类实现了一个方法,当不再需要该对象时必须调用该方法,并且需要自行清理(将状态保存到文件系统,释放锁等)。 必须在垃圾收集器运行之前调用此方法(即在调用对象的终结器之前):
public class Sqm { var values = new List(); Boolean shutdown = false; protected void Cleanup(ICollection stuff) { WebRequest http = new HttpWebRequest(); http.Open("POST", "https://stackoverflow.com/SubmitUsageTelemetry"); http.PostBody = stuff; http.Send(); } public void Shutdown() { if (!alreadyShutdown) { Cleanup(values); alreadyShutdown = true; } } }
何时,何地,我可以调用我的Shutdown()
方法?
注意 :我不希望使用 Sqm
类的开发人员担心调用Shutdown
。 那不是他的工作。 在其他语言环境中,他不必这样做。
Lazy
类似乎没有在它懒惰拥有的Value
上调用Dispose
。 所以我无法挂钩IDisposable
模式 – 并将其用作调用Shutdown
的时间。 我需要自己打电话给Shutdown
。
但当?
它是一个static
变量,它在应用程序/域/ appdomain / apartment的生命周期中存在一次。
是的,终结者是错误的时间
有些人确实理解,而有些人却不明白,在finalizer
器中尝试上传我的数据是错误的 。
///WRONG: Don't do this! ~Sqm { Shutdown(_values); //<-- BAD! _values might already have been finalized by the GC! }
为什么这是错的? 因为values
可能不再存在了。 您无法控制以什么顺序最终确定的对象。 完全有可能在包含Sqm
之前确定values
。
处置怎么样?
IDisposable
接口和Dispose()
方法是一种约定 。 如果我的对象实现了一个将被调用的Dispose()
方法,那就没有任何规定。 事实上,我可以继续实施它:
public class Sqm : IDisposable { var values = new List(); Boolean alreadyDiposed = false; protected void Cleanup(ICollection stuff) { WebRequest http = new HttpWebRequest(); http.Open("POST", "https://stackoverflow.com/SubmitUsageTelemetry"); http.PostBody = stuff; http.Send(); } public void Dispose() { if (!alreadyDiposed) { Cleanup(values); alreadyDiposed = true; } } }
对于实际阅读问题的人,您可能会注意到我实际上没有改变任何东西。 我唯一做的就是将方法的名称从Shutdown更改为Dispose 。 Dispose模式只是一种约定。 我还有问题:我什么时候可以拨打Dispose
?
那么你应该从你的终结者那里打电话给你
从我的终结器调用Dispose
与从终结器调用Shutdown
一样不正确(它们完全相同):
public class Sqm : IDisposable { var values = new List(); Boolean alreadyDiposed = false; protected void Cleanup(ICollection stuff) { WebRequest http = new HttpWebRequest(); http.Open("POST", "https://stackoverflow.com/SubmitUsageTelemetry"); http.PostBody = stuff; http.Send(); } public void Dispose() { if (!alreadyDiposed) { Cleanup(_values); // <--BUG: _values might already have been finalized by the GC! alreadyDiposed = true; } } ~Sqm { Dispose(); } }
因为, values
可能再也不存在了。 为了完整起见,我们可以返回完整的原始正确代码:
public class Sqm : IDisposable { var values = new List(); Boolean alreadyDiposed = false; protected void Cleanup(ICollection stuff) { WebRequest http = new HttpWebRequest(); http.Open("POST", "https://stackoverflow.com/SubmitUsageTelemetry"); http.PostBody = stuff; http.Send(); } protected void Dispose(Boolean itIsSafeToAlsoAccessManagedResources) { if (!alreadyDiposed) { if (itIsSafeToAlsoAccessManagedResources) Cleanup(values); alreadyDiposed = true; } } public void Dispose() { this.Dispose(true); GC.SuppressFinalize(this); } ~Sqm { Dispose(false); //false ==> it is not safe to access values } }
我已经完整了。 我有一个对象,我需要在应用程序域关闭之前“清理” 。 当我可以调用Cleanup
时,需要通知我的对象内部的某些内容。
让开发人员调用它
没有。
我正在将现有概念从另一种语言迁移到C#中。 如果开发人员碰巧使用全局单例实例:
Foo.Sqm.TimerStart();
那么Sqm
类是懒惰初始化的。 在(本机)应用程序中,保持对对象的引用。 在(本机)应用程序关闭期间,保存接口指针的变量设置为null
,并且调用单例对象的destructor
,并且它可以自我清理。
任何人都不应该打电话。 不Cleanup
,不Shutdown
,不Dispose
。 关闭应该由基础设施自动发生。
什么是C#相当于我看到自己走开,清理自己 ?
如果你让垃圾收集器收集对象这一事实很复杂:现在为时已晚。 我想要保留的内部状态对象可能已经完成。
如果来自ASP.net会很容易
如果我可以保证我的类是从ASP.net使用的,我可以要求HostingEnvironment
在域关闭之前通过注册我的对象来通知它:
System.Web.Hosting.HostingEnvironment.RegisterObject(this);
并实现Stop
方法:
public class Sqm : IDisposable, IRegisteredObject { var values = new List(); Boolean alreadyDiposed = false; protected void Cleanup(ICollection stuff) { WebRequest http = new HttpWebRequest(); http.Open("POST", "https://stackoverflow.com/SubmitUsageTelemetry"); http.PostBody = stuff; http.Send(); } protected void Dispose(Boolean itIsSafeToAlsoAccessManagedResources) { if (!alreadyDiposed) { if (itIsSafeToAlsoAccessManagedResources) Cleanup(values); alreadyDiposed = true; } } public void Dispose() { this.Dispose(true); GC.SuppressFinalize(this); } Sqm { //Register ourself with the ASP.net hosting environment, //so we can be notified with the application is shutting down HostingEnvironment.RegisterObject(this); //asp.net will call Stop() when it's time to cleanup } ~Sqm { Dispose(false); //false ==> it is not safe to access values } // IRegisteredObject protected void Stop(Boolean immediate) { if (immediate) { //i took too long to shut down; the rug is being pulled out from under me. //i had my chance. Oh well. return; } Cleanup(); //or Dispose(), both good } }
除了我的课程不知道我是从ASP.net , WinForms , WPF ,控制台应用程序,还是shell扩展调用。
编辑 :人们似乎对IDisposable
模式的存在感到困惑。 删除了对Dispose
引用以消除混淆。
编辑2 :在他们回答问题之前,人们似乎要求完整,详细的示例代码。 我个人认为这个问题已经包含了太多的示例代码,因为它不能帮助提出问题。
现在我已经添加了太多的代码,问题已经丢失了。 在问题合理之前,人们拒绝回答问题。 既然这是合理的,没有人会读它。
这就像诊断
它就像System.Diagnostics.Trace
类。 人们在他们想要的时候叫它:
Trace.WriteLine("Column sort: {0} ms", sortTimeInMs);
而且再也不用想了。
然后绝望陷入困境
我甚至非常绝望,我考虑将我的对象隐藏在COM IUnknown
接口后面,这是引用计数
public class Sqm : IUnknown { IUnknown _default = new Lazy(); }
然后希望我可以欺骗 CLR减少我的界面上的引用计数。 当我的引用计数变为零时,我知道一切都在关闭。
其缺点是我无法使其发挥作用。
这里有两个问题:
-
您坚持认为
List
可能已经完成。List
没有终结器,它也不会被垃圾收集(因为你有一个引用它)。 (这些是不同的操作。)您的SQL
终结器仍将看到有效数据。 实际上,终结器可能没问题 – 虽然当终结器运行时,你需要的其他资源可能已经消失了 – 终结器甚至可能不会被调用。 所以我认为这同时比你预期的更可行 – 而且总的来说更糟糕。 -
你坚持认为你不想把它放在开发者的控制之下,无论是否使用
IDisposable
。 这只是与.NET提供的内容作斗争。 垃圾收集器用于存储器资源; 任何需要确定性清理(包括刷新等)的非内存资源都应该明确清理。 您可以使用终结器作为最后的“尽力而为”清理,但不应该以您尝试使用它的方式使用它。
您可以使用一些方法来尝试解决此问题,例如使用“canary”对象并引用“真实”对象:保留对您在其他地方感兴趣的对象的强引用,并使用终结器只是在金丝雀对象中,所以唯一要确定的是金丝雀对象 – 然后触发适当的刷新并删除最后一个强引用,使真实对象符合GC的条件 – 但它仍然是一个根本不好的主意,并且混合中的静态变量变得更糟。
同样,您可以使用AppDomain.DomainUnload
事件 – 但同样,我不会。 当域被卸载时,我会担心其余对象的状态 – 并且不会调用默认域。
基本上,我认为你应该改变你的设计。 我们并不真正了解您尝试设计的API的背景,但您现在的方式将无法正常工作。 我会尽量避免使用静态变量 – 至少对于任何在时间方面很重要的事情都是如此。 幕后可能仍然有一个对象用于协调,但是在你的API中暴露出来对我来说是个错误。 无论你如何抗议其他语言和其他平台,如果你在.NET工作,你需要接受它就是它。 从长远来看,与系统作斗争并不会对你有所帮助。
您越早得出结论,您需要更改API设计,您需要花费更多时间来考虑新API应该是什么样子。
AppDomain上有一个ProcessExit
事件你可以尝试挂钩,虽然我不太了解它,它的默认时间限制为2秒。
这样的东西(如果它适合你);
class SQM { static Lazy _Instance = new Lazy ( CreateInstance ); private static SQM CreateInstance() { AppDomain.CurrentDomain.ProcessExit += new EventHandler( Cleanup ); return new SQM(); } private static Cleanup() { ... } }
除了Ken的答案之外,还有“如何丢弃我的物体?”的答案。 是的,你不能。
您正在寻找的概念是静态解构函数,或者在释放静态方法时运行的解构函数。 这在托管代码中不存在 ,并且在大多数(所有?)情况下不应该是必需的。 当可执行文件结束时,您很可能会查看正在卸载的静态方法,操作系统将在此时清除所有内容。
如果您绝对需要释放资源,并且必须在所有活动实例之间共享此对象,则可以创建引用计数器并在确定已释放所有引用时处置该对象。 我会首先考虑这是否是正确的方法。 新实例需要validation您的对象是否为null
,如果是,则再次实例化它。
您不应该调用Dispose
。 如果实现IDisposable
的类仅使用托管资源,那么这些资源将在程序完成时自然发布。 如果类使用未受管理的资源,那么该类应该扩展CriticalFinalizerObject
并在其终结器(以及其Dispose
方法)中释放这些资源。
换句话说,正确使用IDisposable
接口不需要调用Dispose
。 可以调用它来在程序中的特定点释放托管或非托管资源,但是由于不调用它而发生的泄漏应该被视为错误。
编辑
什么是C#相当于我看到自己走开,清理自己 ?
在回答您编辑过的问题时,我认为您正在寻找终结者:
class Foo { ~Foo() { // Finalizer code. Called when garbage collected, maybe... } }
但请注意,不能保证调用此方法。 如果你绝对需要它,你应该扩展System.Runtime.ConstrainedExecution.CriticalFinalizerObject
。
不过,我可能仍会对你的问题感到困惑。 终结器绝对不是 “将我的内部值保存到文件中”的地方。
AppDomain域卸载事件似乎非常适合您正在寻找的内容。 由于静态变量一直存在,直到AppDomain被卸载,这应该在变量被销毁之前给你一个钩子。
你花了这么多时间来对抗语言,为什么不重新设计以便问题不存在?
例如,如果您需要保存变量的状态,而不是在它被销毁之前尝试捕获它,请在每次修改时保存它,并覆盖以前的状态。
如果有四种不同的方式,我曾四次问过这个问题。 每个人的措辞略有不同; 试图从不同的方向解决问题。 最后是MA Hanin指出了我解决这个问题的问题。
问题是没有一种方法可以知道域何时关闭。 你能做的最好的事情就是尝试捕捉100%(四舍五入到最接近的百分比)时间的各种事件。
如果代码在默认的某个域中,则使用DomainUnload
事件。 不幸的是, 默认的 AppDomain不会引发DomainUnload
事件。 那么我们捕获ProcessExit
:
class InternalSqm { //constructor public InternalSqm () { //... //Catch domain shutdown (Hack: frantically look for things we can catch) if (AppDomain.CurrentDomain.IsDefaultAppDomain()) AppDomain.CurrentDomain.ProcessExit += MyTerminationHandler; else AppDomain.CurrentDomain.DomainUnload += MyTerminationHandler; } private void MyTerminationHandler(object sender, EventArgs e) { //The domain is dying. Serialize out our values this.Dispose(); } ... }
这已经在“网站”和“WinForms”应用程序中进行了测试。
代码越完整,显示了IDisposable
的实现:
class InternalSqm : IDisposable { private Boolean _disposed = false; //constructor public InternalSqm() { //... //Catch domain shutdown (Hack: frantically look for things we can catch) if (AppDomain.CurrentDomain.IsDefaultAppDomain()) AppDomain.CurrentDomain.ProcessExit += MyTerminationHandler; else AppDomain.CurrentDomain.DomainUnload += MyTerminationHandler; } private void MyTerminationHandler(object sender, EventArgs e) { //The domain is dying. Serialize out our values this.Dispose(); }
。
/// /// Finalizer (Finalizer uses the C++ destructor syntax) /// ~InternalSqm() { Dispose(false); //False: it's not safe to access managed members } public void Dispose() { this.Dispose(true); //True; it is safe to access managed members GC.SuppressFinalize(this); //Garbage collector doesn't need to bother to call finalize later } protected virtual void Dispose(Boolean safeToAccessManagedResources) { if (_disposed) return; //be resilient to double calls to Dispose try { if (safeToAccessManagedResources) { // Free other state (managed objects). this.CloseSession(); //save internal stuff to persistent storage } // Free your own state (unmanaged objects). // Set large fields to null. Etc. } finally { _disposed = true; } } }
样品用法
从进行图像处理的库中:
public static class GraphicsLibrary { public Image RotateImage(Image image, Double angleInDegrees) { Sqm.TimerStart("GraphicaLibrary.RotateImage"); ... Sqm.TimerStop("GraphicaLibrary.RotateImage"); } }
从可以执行查询的帮助程序类
public static class DataHelper { public IDataReader ExecuteQuery(IDbConnection conn, String sql) { Sqm.TimerStart("DataHelper_ExecuteQuery"); ... Sqm.TimerStop("DataHelper_ExecuteQuery"); } }
对于WinForms主题绘图
public static class ThemeLib { public void DrawButton(Graphics g, Rectangle r, String text) { Sqm.AddToAverage("ThemeLib/DrawButton/TextLength", text.Length); } }
在网站中:
private void GetUser(HttpSessionState session) { LoginUser user = (LoginUser)session["currentUser"]; if (user != null) Sqm.Increment("GetUser_UserAlreadyFoundInSession", 1); ... }
在扩展方法中
/// /// Convert the guid to a quoted string /// /// A Guid to convert to a quoted string /// public static string ToQuotedStr(this Guid source) { String s = "'" + source.ToString("B") + "'"; //B=braces format "{6CC82DE0-F45D-4ED1-8FAB-5C23DE0FF64C}" //Record how often we dealt with each type of UUID Sqm.Increment("String.ToQuotedStr_UUIDType_"+s[16], 1); return s; }
注意 :任何代码都将发布到公共域中。 无需归属。
它们将持续AppDomain的持续时间。 对方法可见对静态变量所做的更改。
MSDN:
如果使用Static关键字声明局部变量,则其生命周期长于声明它的过程的执行时间。 如果过程在模块内部,只要应用程序继续运行,静态变量就会存在。