处理多个应用程序使用的库中的非托管资源的优雅方式

时间:2012-07-12 06:25:01

标签: c# singleton unmanaged idisposable

我的应用程序中的库需要全局使用非托管资源。

为方便起见,该库具有以下内容:

  • 有一个继承自IDisposable的单例类。
  • 获取非托管资源发生在类的Instance()方法中。
  • 将始终通过MySingleton对象访问资源,因此这似乎是确保非托管资源在需要时可用的合理方式。

    public class MySingleton : IDisposable
    {
        private static MySingleton instance;
    
        public static MySingleton Instance
        {
            get
            {
                if (instance == null)
                {
                    lock (typeof(MySingleton))
                    {
                        if (instance == null)
                        {
                            instance = new MySingleton();
    
                            // Acquire unmanaged resource here
                        }
                    }
                }
    
                return instance;
            }
        }
    
        public void Dispose()
        {
            // Release unmanaged resource here
        }
    }
    

    问题

    • 具有上述单例的库正在被多个应用程序使用。
    • 为了确保正确清理非托管资源,我需要在每个应用程序中调用MySingleton.Instance.Dispose()(通常在finally块中,以确保即使在exception情况下也会发生这种情况
    • 如果创建了另一个应用程序(即入口点)且作者忘记调用Dispose方法,则可能导致未正确清理非托管资源。

    我尝试在MySingleton类中添加了一个析构函数来执行此操作,但在应用程序退出时似乎没有出现断点。我猜这是因为GC是非确定性的,也许应用程序在静态对象的析构函数被销毁之前结束,但我不确定。

    是否有一种优雅的方法可以确保始终清理此资源,而无需依赖显式调用Dispose的客户端应用程序来确保这种情况发生?

  • 3 个答案:

    答案 0 :(得分:2)

    对单例的静态引用将使其保持活动状态,无论其他人是否实际使用它。如果您希望检测到您的单身人士的所有外部引用何时消失而不让自己保持活着,则必须执行以下某项操作:

    1. 将“WeakReference”保留给单例,并在每次有人请求对象实例时返回。您可能希望保持长和短的“WeakReference”;一旦对象有资格最终确定,一个短的将被无效;在对象完成后,GC循环将保持有效。如果一个简短的`WeakReference`失效,即使你有一个很长的弱引用,即使终结器还没有运行,也可能需要创建一个新的实例。另一方面,您可能需要一个长的'WeakReference`来帮助处理在旧对象上请求终结器与实际运行的时间之间请求新对象的情况。
    2. 保持对“真实”单身人士的强烈引用,但绝不会将其暴露给任何人。相反,创建一个包装器并将“WeakReference”存储到它。包装器的终结器应该请求单例自行清理。这种方法可能比第一种方法更安全,在这种情况下,代码在旧对象符合最终确定条件和实际发生时间确定之间要求单例实例。如果通过单个`Finalize`方法进行所有清理,则get-singleton方法可以请求终结器跳过清理,并知道它是否及时发出请求(如果没有,则可能必须等待终结器完成才能创建新实例)。
    3. 保持对`real`单例的强引用,并为每个不同的实体提供一个不同的包装器。保留一个列表,其中包含哪些包装器尚未调用`finalize`(最多保留对包装器本身的弱引用,但保留对它的信息的强引用),并且当列表为空时,执行清理。这种方法比第二种方法更复杂,但它可以确定谁持有对象的引用。

    如果您的资源是在旧实例存在时无法重新创建的资源,那么我的建议通常是方法#2。方法#1的问题在于,如果有多个可终结对象与资源相关联,则可能存在大量间隔,在此期间资源被部分清理,因此代码可能别无选择,只能等待其他对象'终结者完成。当使用方法#2时,如果在终结器处于挂起状态时请求单例,则请求将很快到来以防止终结器执行清理(在这种情况下,它不需要等待终结器运行,因为终结器赢了'实际上做了什么),它将在终结器运行之后(在这种情况下可以立即创建新实例),或者在终结器执行清理时(这是最坏的情况 - 代码必须等待)对象自己的清理要完成,但不必等待其他人的最终确定。)

    答案 1 :(得分:1)

    单独使用IDisposable听起来有误。谁负责处理该物体?

    当两个线程或两个进程尝试访问资源时应该发生什么?失败者应该阻止或抛出异常吗?

    我认为你最好采用RAII方法,你有一个实现IDisposable的类,并且只能在资源正在使用的时候生存:

    class MyResource : IDisposable { 
      public MyResource() {
        // Acquire resource here
      }
    
      public void Dispose() {
        // Free resource here, along with extra stuff to attempt
        // to catch situations where its not disposed 
      }
    }
    

    然后:

    using( var r = new MyResource() ) {
      // Do work here.
    }
    

    你不能保证使用你的库的每个其他开发人员都会记得正确地处理这个类,但是任何未能在using语句中使用该类的人都可以用一个大棒的方法进行教育。

    答案 2 :(得分:0)

    关于你的析构函数没有被击中,请看这里: http://msdn.microsoft.com/en-us/library/66x5fx1b.aspx 析构函数被调用,但我不认为你可以在调试时看到它。 至于你的单身人士,显然在程序退出之前不会被调用,因为它是静态的。

    关于确保调用Dispose,我认为你能做的最好就是继承IDisposable并记录需要处理。如果有办法自动处理事物,你不觉得MS会对SqlConnection或任何其他一次性类做过吗?顺便说一句,我认为确定何时将一个实例放置在类本身中是不好的做法。这样就限制了你的类的使用,你无法知道将使用你的类的其他组件想要做什么。