9

我什么时候可以清理存储在 C# 中的静态变量中的对象?

我有一个延迟初始化的静态变量:

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 个,它也无法帮助我解决问题(何时“清理”静态成员)。

示例使用

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>似乎没有调用它懒惰地拥有Dispose的东西。Value所以我不能挂钩IDisposable模式 - 并将其用作调用的时间Shutdown。我需要给Shutdown自己打电话。

但当?

它是一个static变量,它在应用程序/域/应用程序域/公寓的整个生命周期中存在一次。

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

有些人明白,有些人不明白,在 a 期间尝试上传我的数据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()将被调用的方法,则没有任何规定。事实上,我可以继续实施它:

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

好吧,您应该从终结器中调用 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可能不再存在。为了完整起见,我们可以返回完整的原始正确代码:

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();

然后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<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:人们在回答问题之前似乎要求完整、详细的示例代码。我个人认为这个问题已经包含了太多的示例代码,因为它不能帮助提出这个问题。

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

这就像诊断

就像System.Diagnostics.Trace上课一样。人们在需要时调用它:

Trace.WriteLine("Column sort: {0} ms", sortTimeInMs);

并且永远不必再想它。

然后绝望降临

我什至非常绝望,我考虑将我的对象隐藏在一个 COMIUnknown接口后面,它是引用计数的

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

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

这样做的缺点是我无法让它工作。

4

8 回答 8

12

这里有两个问题:

  • 你坚持认为List<string>可能已经完成。List<string>没有终结,它还没有被垃圾收集(因为你有它的引用)。(这些是不同的操作。)您的终结器仍将看到有效数据。所以实际上,终结器可能没问题 - 尽管当终结器运行您需要的其他一些资源时可能已经消失了 - 终结器甚至可能最终都不会被调用。所以我认为这同时比你预期的更可行——而且总体上是一个更糟糕的想法。SQL

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

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

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

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

您越早得出需要更改 API 设计的结论,就越有时间考虑新 API 应该是什么样子。

于 2013-08-17T07:14:36.480 回答
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()
    {
        ...
    }

}
于 2013-08-15T14:05:25.100 回答
1

如果有四种不同的方式,我已经问过这个问题四次了。措辞略有不同;试图从不同的方向解决问题。最终是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();
   }

.

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

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

于 2013-08-19T14:29:15.073 回答
0

你不应该打电话给Dispose. 如果实现的类IDisposable只使用托管资源,那么这些资源将在程序完成时自然释放。如果该类使用非托管资源,则该类应CriticalFinalizerObject在其终结器(以及其Dispose方法)中扩展和释放这些资源。

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

编辑

什么是我看到自己离开,清理自己的 C# 等价物?

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

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

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

不过,我可能仍然对你的问题感到困惑。终结器绝对不是“将我的内部值保存到文件”的地方。

于 2013-08-02T15:43:22.747 回答
0

除了 Ken 的回答之外,“如何处置我的对象?”的回答。是,你不能。

您正在寻找的概念是静态解构器,或者在释放静态方法时将运行的解构器。这在托管代码中不存在,并且在大多数(所有?)情况下不应该是必需的。当可执行文件结束时,您很可能会查看正在卸载的静态方法,并且操作系统将在此时清理所有内容。

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

于 2013-08-02T15:47:20.913 回答
0

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

于 2013-08-16T20:00:40.117 回答
0

您一直在与语言作斗争,为什么不重新设计以使问题不存在?

例如,如果你需要保存一个变量的状态,而不是试图在它被销毁之前捕获它,而是在每次修改它时保存它,并覆盖之前的状态。

于 2013-08-21T19:20:15.917 回答
-1

它们将在 AppDomain 期间持续存在。对静态变量所做的更改在方法中是可见的。

MSDN:

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

于 2013-08-02T15:41:28.823 回答