如何在静态变量最终化之前接收通知

10
什么时候可以在C#中清理存储在静态变量中的对象?
我有一个静态变量是懒加载的:
public class Sqm
{
    private static Lazy<Sqm> _default = new Lazy<Sqm>();

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

注意:我刚把 Foo 改成了一个静态类。如果 Foo 是静态的或者不是静态的,这并不会改变问题。但是有些人认为没有办法在构造 Sqm 实例之前构造 Foo 实例。即使我创建一个 Foo 对象; 即使我创建 100 个对象,这也无法帮助我解决(何时“清理”静态成员)的问题。
用法示例
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>类似乎没有在它懒惰地拥有的Value上调用Dispose。因此,我无法挂钩IDisposable模式 - 并将其用作调用Shutdown的时间。我需要自己调用Shutdown

但是在什么时候?

它是一个静态变量,它存在于应用程序/域/应用程序域/公寓的生命周期内。

是的,终结器的时间不对

有些人理解,有些人不理解,在finalizer期间尝试上传我的数据是错误的

///WRONG: Don't do this!
~Sqm
{
   Shutdown(_values); //<-- BAD! _values might already have been finalized by the GC!
}   

为什么这是错误的?因为values可能已经不存在了。您无法控制对象以何种顺序进行终结。完全有可能在包含Sqm之前,values就已经被终结了。 < h1 >那么Dispose呢? < p > IDisposable接口和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;
      }
   }
}

针对实际阅读此问题的人,您可能会注意到我实际上没有改变任何内容。我唯一做的是将方法的名称从Shutdown更改为Dispose。Dispose模式只是一种约定。我的问题仍然存在:何时可以调用Dispose

那你应该从finalizer中调用Dispose

从我的finalizer调用Dispose与从我的finalizer调用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可能不再存在。为了完整起见,我们可以返回到完整的原始正确代码:

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
   }
}

我已经完整地经历了一次循环。我有一个需要在应用程序域关闭之前“清理”的对象。我的对象中的某些内容需要在可以调用 Cleanup 时得到通知。

让开发者调用它

不行。

我正在将从另一种语言中现有的概念迁移到C#。如果开发者恰好使用全局单例实例:

Foo.Sqm.TimerStart();

如果是(native)应用程序,那么Sqm类会进行惰性初始化。在(native)应用程序中,会持有对该对象的引用。在(native)应用程序关闭期间,保存接口指针的变量会被设置为null,并且单例对象的析构函数会被调用,它可以自行清理。

没有人应该去调用任何东西。不是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<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
   }
}

除了我的班级不知道我会从ASP.netWinFormsWPF、控制台应用程序或Shell扩展中被调用。 编辑: 人们似乎对为什么存在IDisposable模式感到困惑。为了消除困惑,删除了对Dispose的引用。 编辑2: 人们似乎要求在回答问题之前提供完整详细的示例代码。个人认为问题已经包含了太多的示例代码,因为它并没有帮助提出问题。
现在我添加了sooo多的代码,问题已经丢失了。人们拒绝回答问题,直到问题得到证明。现在问题已经得到证明,没有人会阅读它。

就像诊断一样

这就像System.Diagnostics.Trace类。人们在需要时调用它:
Trace.WriteLine("Column sort: {0} ms", sortTimeInMs);

我希望你永远不必再去考虑它。

然后绝望开始了

我甚至绝望到考虑将我的对象隐藏在一个COM IUnknown接口后面,这个接口是引用计数的。

public class Sqm : IUnknown
{
   IUnknown _default = new Lazy<Sqm>();
}

然后希望我可以欺骗CLR来减少我的接口引用计数。当我的引用计数变为零时,我知道一切都在关闭。

缺点是我无法使其工作。


2
这是一个静态变量。它属于类,只有在 Foo 被销毁时才会被销毁。 - Nick
@Nick Foo 可能永远不会被创建,因此我将无法知道它何时被销毁。 - Ian Boyd
如果创建了 Foo,那么它只会在卸载 AppDomain 时被销毁。 - Sriram Sakthivel
如果Foo从未被创建,那么Sqm也永远不会被创建,对吗?你的问题是什么?如果你在谈论默认的AppDomain,那么没有办法得到通知。如果你自己创建了AppDomain,那么可以订阅DomainUnload事件 - Sriram Sakthivel
1
@IanBoyd:这很重要,因为不同的项目类型具有不同的生命周期。有些比其他项目类型更多地通知关闭。 - Jon Skeet
显示剩余5条评论
8个回答

13

这里有两个问题:

  • 您坚持认为List<string>可能已经被终结了。 List<string>没有终结器,并且它尚未被垃圾收集(因为您有对它的引用)。 (这些是不同的操作。)您的SQL终结器仍将查看有效数据。实际上,终结器也许是可以的 - 虽然到终结器运行时,您需要的一些其他资源可能已经消失了 - 并且终结器甚至可能不会被调用。所以我认为这个想法同时比您预期的更可行 - 但一般来说更糟糕。

  • 您坚持认为不希望通过开发人员控制来使其确定化,无论是使用IDisposable还是不使用。这只是与.NET提供的相抵触。垃圾收集器的用途是用于内存资源;任何需要确定性清理(包括刷新等)的非内存资源都应该显式清理。您可以将终结器用作最后的“最佳努力”清理,但不应该像您尝试使用它的那样使用它。

有一些方法可以尝试解决此问题,例如使用具有对“真实”对象引用的“金丝雀”对象:在其他地方保留对您感兴趣的对象的强引用,并仅在金丝雀对象中拥有终结器,以便唯一要终止的是金丝雀对象 - 然后触发适当的清空并删除最后一个强引用,使真实对象符合GC的条件 - 但这仍然基本上是个糟糕的想法,加入静态变量会让它变得更糟。

同样,您可以使用AppDomain.DomainUnload事件 - 但我不会这样做。当域被卸载时,我会担心其他对象的状态,并且默认域不会调用该事件。
基本上,我认为你应该改变你的设计。我们不知道你正在尝试设计的API的背景,但是你目前的方式行不通。对于任何重要的时间问题,我个人会尝试避免使用静态变量。在协调方面仍然可以有一个单独的对象,但在API中公开它感觉就不对了。无论您多么强烈地反对其他语言和平台,如果您在.NET中工作,您需要接受它是什么样子。长期抗争系统不会对您有所帮助。
您越早得出需要更改API设计的结论,您就有更多的时间考虑新API应该是什么样子。

如果你将 List<String> 替换为 List<AnythingElse>,你可以观察到子 AnythingElse 对象在列表本身被终结之前被终结。而且列表在我的 Sqm 类被终结之前被终结。设计的思路是任何地方的代码都可以调用 Sqm.AddSample(...)。如果开发人员必须构造一个类的实例,将引用保存在某个地方,并调用某些东西来“关闭”,那么他们(指我或同事)就不会使用它。所以这就是要么什么都没有。 - Ian Boyd
2
@IanBoyd:它们可能已经被finalized了,但它们不会被垃圾回收。你从未说过这是一个finalizable类型的列表(在我的经验中非常罕见 - 你经常编写finalizers吗?)- 你说这是一个字符串列表。但基本上,如果“这个设计根本违反了.NET原则,要么就什么都不做”,那么答案听起来就是“什么都不做”- 或者你接受降低可靠性(例如每10秒刷新并接受潜在数据丢失)。 - Jon Skeet
@IanGriffiths:确实,在一般情况下它并不有用,但在某些特定的情境下可能会有用。(这就是为什么我添加了一条评论,询问上下文细节。) - Jon Skeet
@JonSkeet 是的 - 抱歉,我忽视了您确实提到默认域不会引发异常的事实。已删除我的评论。 - Ian Griffiths
这不是一个字符串列表,而是一个不属于我的对象列表。如果其中任何一个对象有终结器,那么它将无法工作。 - Ian Boyd
1
@IanBoyd:你不觉得直接说清楚会比写一个误导性的问题更有帮助吗?再说一遍,我认为使用终结器并不是正确的方法。(如果你真的想使用终结器,那么一个"canary"对象来警告终结可能是可行的,但你仍然在一种终结器本身并不打算用于的方式依赖终结器。) - Jon Skeet

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()
    {
        ...
    }

}

1
我已经用四种不同的方式提出了这个问题,每一种方式都稍微有所不同;试图从不同的方向解决问题。最终是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();
   }

   ...
}

这已经在一个名为“web-site”的网站和一个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;
}

注意:任何代码都已发布到公共领域,不需要署名。


1
这是相当危险的 - 如果你进入了ProcessExit情况,并且超过了2秒钟,你的数据将会丢失。考虑到你的原始示例似乎涉及HTTP post,你很可能会遇到这个问题,不是吗?再加上异常终止的问题,整个方法看起来都不可靠。我倾向于在不活动超时时保存数据,而不是等到可能为时已晚。如果这意味着需要稍微频繁地保存一些数据,那就这样做吧。 - Ian Griffiths
@IanGriffiths 保存时的问题在于滚动文件模型。因此,每次我的GUI Contoso.exe 关闭时,都会写出一个文件 Contoso-sqmxx.xml。其中 XX 滚动到最大值(例如,从 Contoso-sqm00.xmlContoso-sqm09.xml)。如果存在 Contoso-sqm000102,则保存到 03。如果我每 n 秒保存一次,那么很快就会翻转其他文件。 - Ian Boyd
这并不一定是一个终止问题。您可以继续替换最后一个文件 - 因此当您编写数据时,请勿丢弃它,下一次只需编写一个更大的文件即可。(为了更加稳健,我会将其写入某个临时名称,例如“Contoso-sqm02.xml”,然后在成功写入后,删除“Contoso-sqm02.xml”并将“Contoso-sqm02.xml”重命名为“Contoso-sqm02.xml”) - Ian Griffiths

0

您不需要调用Dispose。如果实现了IDisposable接口的类仅使用托管资源,则这些资源将在程序完成时自然释放。如果该类使用非托管资源,则该类应扩展CriticalFinalizerObject并在其终结器中释放这些资源(以及在其Dispose方法中释放)。

换句话说,正确使用IDisposable接口不需要调用Dispose。可以在程序的特定点调用它来释放托管或非托管资源,但因为调用它而导致泄漏的情况应被视为错误。

编辑

我看到自己走了,打扫干净身后的C#等效物是什么?

针对您编辑后的问题,我认为您正在寻找终结器:

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

请注意,这种方法并不能保证一定会被调用。如果您绝对需要它被调用,您应该扩展System.Runtime.ConstrainedExecution.CriticalFinalizerObject
不过,我可能仍然对您的问题感到困惑。finalizer 绝对不是“将我的内部值保存到文件”的地方。

2
“终结器绝对不是将我的内部值保存到文件的地方” 您是正确的!这就是我要解决的问题!人们一直混淆“自我清理”与“释放资源”,并想要向我解释垃圾回收器如何为我释放资源。这不是我在问什么;我不是在问如何释放资源 - 我在问“清理”。在被销毁之前必须完成的那些事情。是的,垃圾回收器会炸掉我的房子;但在它被摧毁之前,我想将最后的支票寄给城市。” - Ian Boyd

0
除了Ken的回答之外,“如何处理我的对象?”的答案是,你不能。
你要找的概念是静态析构函数,或者在释放静态方法时运行的析构函数。这在托管代码中不存在,在大多数(所有?)情况下也不应该必要。当可执行文件结束时,您很可能正在查看静态方法被卸载,操作系统将在那时清理所有内容。
如果您绝对需要释放资源,并且此对象必须在所有活动实例之间共享,则可以创建引用计数器,并在确保已释放所有引用后处理对象。首先,我会认真考虑这是否是正确的方法。新实例需要验证您的对象是否为null,如果是,则再次实例化它。

我不想要“释放资源”。那个措辞是错误的。我需要“关闭”我的对象。换句话说,用户调用Foo.Sqm.AddString("Hello")Sqm类需要将该字符串保存到硬盘中。在垃圾回收期间保存这些字符串已经太晚了。 - Ian Boyd
@IanBoyd 你可以考虑在 Sqm 上使用 finalizer,但要注意,在对象被处理的情况下,finalizer 不会被调用。最好和更可靠的方法是在需要时(关闭或其他时间)调用 Sqm.Save()。也许可以将 Foo 设为 IDisposable 并使用引用计数。 - Will Eddins
如果我放置了GC.SuppressFinalize(this),那么终结器将不会被调用。在我的Dispose方法中,我可以选择调用GC.SuppressFinalize(this)。但是问题变成了如何让某人调用Dispose(因为我不希望开发人员这样做)。 - Ian Boyd

0
你花费了很多时间在与语言作斗争,为什么不重新设计以消除这个问题呢?
例如,如果你需要保存变量的状态,不要试图在它被销毁之前捕获它,而是每次修改时保存它,并覆盖先前的状态。

1
问题在于写出状态可能需要几十毫秒的时间。当调用发生50000次/秒时,速度会变得相对较慢。 - Ian Boyd

0

AppDomain域卸载事件似乎非常适合您的需求。由于静态变量会一直存在,直到AppDomain被卸载,因此这应该可以在变量被销毁之前为您提供一个钩子。


-1

它们将持续整个AppDomain的生命周期。对静态变量所做的更改在方法之间是可见的。

MSDN:

如果使用Static关键字声明局部变量,则其生命周期长于声明它的过程的执行时间。如果该过程位于模块内,则静态变量将在应用程序继续运行时保留。


静态变量会在应用程序运行期间一直存在。当应用程序停止运行时,该变量将被销毁。我需要在应用程序域即将结束时得到通知,以便我可以执行所需的“东西”。虽然“清理”是一个正确的词,但它会让很多人感到困惑,他们会将其与内存回收混淆。 - Ian Boyd

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接