如何在最终确定静态变量之前得到通知

时间:2013-08-02 15:35:06

标签: c# singleton dispose lazy-evaluation

何时可以在C#中清理存储在静态变量中的对象?

我有一个lazily initialized的静态变量:

public class Sqm
{
    private static Lazy<Sqm> _default = new Lazy<Sqm>();

    public static Sqm Default { get { return _default.Value; } }
}

注意:我刚刚将Foo更改为static课程。如果Foo是静态的,它不会以任何方式改变问题。但有些人确信,如果没有先构建Sqm的实例,就无法构建Foo的实例。即使我 创建了一个Foo对象;即使我创造了100个,它也无法帮助我解决问题(何时&#34;清理&#34; 静态成员)。

示例用法

Foo.Default.TimerStart("SaveQuestion");
//...snip...
Foo.Default.TimerStop("SaveQuestion");

现在,我的Sqm类实现了一个方法,当不再需要该对象时,必须调用该方法,并且需要自行清理(将状态保存到文件系统,释放锁等)。在垃圾收集器运行之前(即在调用对象的终结器之前),必须调用此方法

public class Sqm
{
   var values = new List<String>();         
   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<T>班似乎没有在{1}}懒惰地拥有Dispose。所以我无法挂钩Value模式 - 并将其用作调用IDisposable的时间。我需要自己致电Shutdown

但什么时候?

它是Shutdown变量,它在应用程序/域/ appdomain / apartment的生命周期中存在一次。

是的,终结者是错误的时间

有些人确实理解,而有些人并非在static期间尝试上传我的数据 错误

finalizer

为什么这是错的?因为///WRONG: Don't do this! ~Sqm { Shutdown(_values); //<-- BAD! _values might already have been finalized by the GC! } 可能不再存在了。您无法控制按什么顺序最终确定的对象。 values完全可能在包含values之前完成。

处理怎么办?

Sqm界面和IDisposable方法是约定。如果我的对象实现了一个Dispose()方法,那么就没有任何东西能够被调用。事实上,我可以继续实施它:

Dispose()

对于实际阅读问题的人,您可能会注意到我并没有真正改变任何事情。我唯一做的就是将方法的名称从 Shutdown 更改为 Dispose 。 Dispose模式只是一种约定。我还有问题:我什么时候可以拨打public class Sqm : IDisposable { var values = new List<String>(); 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; } } }

你应该从你的终结者调用dispose

从我的终结器调用Dispose与从终结器调用Dispose一样不正确(它们完全相同):

Shutdown

因为public class Sqm : IDisposable { var values = new List<String>(); 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

我已经完整了。我有一个对象,我需要在应用程序域关闭之前&#34;清理&#34; 。当我的对象内部有东西可以调用public class Sqm : IDisposable { var values = new List<String>(); 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 } } 时需要通知。

让开发人员调用它

没有

我将现有概念从另一种语言迁移到C#中。如果开发人员碰巧使用全局单例实例:

Cleanup

然后Foo.Sqm.TimerStart(); 类被延迟初始化。在(本机)应用程序中,保持对对象的引用。在(本机)应用程序关闭期间,保存接口指针的变量设置为Sqm,并且调用单例对象null,它可以清理自己。

任何人都不应该打电话。不是destructor,不是Cleanup,不是Shutdown。关闭应该由基础设施自动发生。

的C#相当于什么?我看到自己离开,自我清理

如果你让垃圾收集器收集对象这一事实很复杂:它已经太晚了。我想要保留的内部状态对象可能已经完成。

如果来自ASP.net

那将很容易

如果我可以保证我的课程是从ASP.net使用的,我可以要求Dispose在域名关闭之前通过注册我的对象来通知:

HostingEnvironment

并实施System.Web.Hosting.HostingEnvironment.RegisterObject(this); 方法:

Stop

除了我的班级不知道我是从 ASP.net ,还是从 WinForms ,或从 WPF < / strong>,或控制台应用程序或shell扩展。

修改:人们似乎对what the IDisposable pattern exists for感到困惑。删除了对public class Sqm : IDisposable, IRegisteredObject { var values = new List<String>(); 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 } } 的引用,以消除混淆。

编辑2 :在他们回答问题之前,人们似乎要求完整,详细的示例代码。我个人认为这个问题已经包含了太多的示例代码,因为它不能帮助提出问题。

现在我已经添加了很多代码 sooo ,但这个问题已经丢失了。在问题合理之前,人们拒绝回答问题。现在它已被证明是合理的,没有人会读它。

它就像诊断

它就像Dispose类。人们在他们想要的时候打电话给他们:

System.Diagnostics.Trace

并且永远不必再考虑它了。

然后绝望设置在

我甚至非常绝望,我考虑将我的对象隐藏在COM Trace.WriteLine("Column sort: {0} ms", sortTimeInMs); 接口后面,这是引用计数

IUnknown

然后希望我能欺骗 CLR来减少我界面上的引用计数。当我的引用计数变为零时,我知道一切都在关闭。

缺点是我无法使其发挥作用。

8 个答案:

答案 0 :(得分:12)

这里有两个问题:

  • 您坚持认为List<string>可能已经完成。 List<string> 没有终结器, (这些是不同的操作。)您的SQL终结器仍会看到有效数据。实际上,终结器可能可以 - 但是当终结器运行时,你需要的某些其他资源可能已经消失了 - 终结器甚至可能不会被终结调用。所以我认为这同时比你预期的更可行 - 而且总的来说更糟糕。

  • 您坚持认为,您不希望通过将其置于开发人员的控制之下来确定这一点,无论是否使用IDisposable。这只是与.NET提供的对抗。垃圾收集器用于内存资源;任何需要确定性清理(包括刷新等)的非内存资源应该明确清理。您可以使用终结器作为最后的“尽力而为”清理,但不应该以您尝试使用它的方式使用它。

您可以 使用某些方法来尝试解决此问题,例如使用“canary”对象并引用“real”对象:保持对对象的强引用你在其他地方感兴趣,并且在金丝雀对象中有一个终结器只是,因此要完成的唯一的事物是金丝雀对象 - 然后触发适当的刷新和删除最后一个强引用,使真实对象符合GC的条件 - 但它仍然是一个坏主意,并且混合中的静态变量会变得更糟。

同样,你可以使用AppDomain.DomainUnload事件 - 但是,我不会。当域被卸载时,我会担心其余对象的状态 - 并且不会调用默认域。

基本上,我认为你应该改变你的设计。我们并不真正了解您尝试设计的API的背景,但您现在的方式将无法正常工作。我会尽量避免使用静态变量 - 至少对于任何在时间方面很重要的事情都是如此。幕后可能还有一个对象用于协调,但是在你的API中暴露出来对我来说感觉就像是一个错误。无论你如何抗议其他语言和其他平台,如果你在.NET中工作,你需要接受它就是它的本质。从长远来看,与系统作斗争并不会对你有所帮助。

越早得出结论,您需要更改API设计,就必须考虑新API应该有多长时间。

答案 1 :(得分:1)

AppDomain上有一个ProcessExit事件你可以尝试挂钩,虽然我不太了解它,它的默认时间限制为2秒。

这样的东西(如果它适合你);

class SQM
{

    static Lazy<SQM> _Instance = new Lazy<SQM>( CreateInstance );

    private static SQM CreateInstance()
    {
        AppDomain.CurrentDomain.ProcessExit += new EventHandler( Cleanup );
        return new SQM();
    }

    private static Cleanup()
    {
        ...
    }

}

答案 2 :(得分:0)

您不需要致电Dispose。如果实现IDisposable的类仅使用托管资源,那么这些资源将在程序完成时自然发布。如果该类使用未受管理的资源,则该类应扩展CriticalFinalizerObject并在其终结器(以及其Dispose方法)中释放这些资源。

换句话说,正确使用IDisposable接口不需要调用Dispose。可以调用它来在程序中的某个特定点释放托管或非托管资源,但是由于调用它而发生的泄漏应被视为错误。

修改

  

的C#相当于什么?我看到自己离开,自我清理

在回答您编辑的问题时,我认为您正在寻找终结者:

class Foo {
    ~Foo() {
        // Finalizer code. Called when garbage collected, maybe...
    }
}

但请注意,不能保证会调用此方法。如果你绝对需要它,你应该扩展System.Runtime.ConstrainedExecution.CriticalFinalizerObject

但是,我仍然会对你的问题感到困惑。终结器肯定是“将我的内部值保存到文件中的地方。”

答案 3 :(得分:0)

除了Ken的回答,“如何处置我的物体?”的答案。是的,你不能。

您正在寻找的概念是静态解构函数,或者在释放静态方法时运行的解构函数。 This does not exist in managed code,并且在大多数(所有?)案例中都不是必需的。当可执行文件结束时,您很可能会看到正在卸载的静态方法,操作系统将在那时清理所有内容。

如果您绝对需要释放资源,并且必须在所有活动实例之间共享此对象,则可以在确定所有引用都已释放时创建引用计数器并处置该对象。我会首先考虑这是否是正确的方法。新实例需要验证您的对象是否为null,如果是,则再次实例化。

答案 4 :(得分:0)

AppDomain Domain Unload Event似乎非常适合您所寻找的内容。由于静态变量一直存在,直到AppDomain被卸载,这应该在变量被销毁之前给你一个钩子。

答案 5 :(得分:0)

如果有四种不同的方式,我曾四次问过这个问题。每个人的措辞略有不同;试图从不同的方向解决问题。最终M.A. Hanin指出我this question解决了这个问题。

问题在于,没有一种方法可以知道域何时关闭。你能做的最好的事情就是尝试捕捉各种各样的事件,覆盖你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();
   }

   /// <summary>
   /// Finalizer (Finalizer uses the C++ destructor syntax)
   /// </summary>
   ~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);

   ...
}

在扩展方法

/// <summary>
/// Convert the guid to a quoted string
/// </summary>
/// <param name="source">A Guid to convert to a quoted string</param>
/// <returns></returns>
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;
}
  

注意:任何代码都会发布到公共域中。无需归属。

答案 6 :(得分:0)

你花了这么多时间来对抗语言,为什么不重新设计以便问题不存在?

e.g。如果你需要保存变量的状态,而不是在它被销毁之前尝试捕获它,每次修改它时保存它,并覆盖以前的状态。

答案 7 :(得分:-1)

它们将持续AppDomain的持续时间。对方法可以看到对静态变量所做的更改。

MSDN:

如果使用Static关键字声明局部变量,则其生命周期长于声明它的过程的执行时间。如果过程在模块内部,只要应用程序继续运行,静态变量就会存活。