【问题标题】:Factory that creates IDisposable objects, how to dispose them?创建 IDisposable 对象的工厂,如何处置它们?
【发布时间】:2018-04-05 15:34:19
【问题描述】:

我的应用程序创建了应该被重用的 IDisposable 对象,所以我创建了一个工厂来封装这些对象的创建和重用,代码如下:

public class ServiceClientFactory
{
    private static readonly object SyncRoot = new object();
    private static readonly Dictionary<string, ServiceClient> Clients = new Dictionary<string, ServiceClient>(StringComparer.OrdinalIgnoreCase);

    public static ServiceClient CreateServiceClient(string host)
    {
        lock(SyncRoot)
        {
            if (Clients.ContainsKey(host) == false)
            {
                Clients[host] = new ServiceClient(host);
            }

            return Clients[host];
        }
    }  
}

public class QueryExecutor
{
    private readonly ServiceClient serviceClient;

    public QueryExecutor(string host)
    {
        this.serviceClient = ServiceClientFactory.CreateServiceClient(host);
    }

    public IDataReader ExecuteQuery(string query)
    {
        this.serviceClient.Query(query, ...);
    }
}

让我头疼的是,ServiceClient 是 IDisposable,我应该在某个时候明确地处理它们。

一种方法是在QueryExecutor中实现IDisposable,在QueryExecutor被释放的时候释放ServiceClient,但是这样,(1)在释放ServiceClient的时候,也需要通知ServiceClientFactory,(2)不能复用ServiceClient实例。

所以我认为让 ServiceClientFactory 管理所有 ServiceClient 实例的生命周期会容易得多,如果我这样做,那么处置工厂创建的所有 IDisposable 对象的最佳做法是什么?挂钩 AppDomain 退出事件并在每个 ServiceClient 实例上手动调用 Dispose()?

【问题讨论】:

  • 很可能是您用来创建连接的任何东西已经使用连接池策略,因此您没有理由构建自己的连接池。正确构建自己的连接池是一项相当大的工作。
  • 请注意,目前您的代码允许多个线程同时与同一个连接进行交互。您的连接对象是否设计为能够被多个线程同时使用?
  • 客户端代码在感觉不再需要连接时调用Dispose。谁以及何时会处置工厂创建的所有对象?池看起来是静态的,这表明它应该仅在进程结束时进行垃圾收集。
  • @Servy 我的对象实际上不是一个连接,只是映像一个 IDisposable 类。
  • 请注意,如果您真的在考虑“挂钩 [ing] AppDomain 退出事件并在每个 ServiceClient 实例上手动调用 Dispose()”,那么您也可以考虑做 nothing。当进程结束时,操作系统将释放所有操作系统级别的资源(套接字、内存、文件句柄)。当然,如果您的一次性用品具有自定义逻辑(例如在某处发送“再见”或刷新文件),那就另当别论了。 Dispose 的核心是尽可能快地确定性地释放资源。如果你永远坚持下去,它基本上就失去了它的目的。

标签: c# factory idisposable


【解决方案1】:

这里有几件事要考虑...

首先,您所描述的似乎是Flyweight pattern 的一些变体。您有一个昂贵的对象ServiceClient,您想重用它,但您希望允许消费者对象随意创建和销毁而不破坏昂贵的对象。 Flyweight 传统上通过引用计数来实现这一点,这可能有点过时了。

您需要确保消费者不能直接处置ServiceClient,因此您还需要一个轻量级的Facade,它拦截对ServiceClient.Dispose 的调用并选择是否处置真实对象。您应该对消费者隐藏真实的ServiceClient

如果一切可行,您可以将您的方法重写为:

// this is the facade that you will work from, instead of ServiceClient
public interface IMyServiceClient : IDisposable
{
    void Query(string query);
}

// This is your factory, reworked to provide flyweight instances
// of IMyServiceClient, instead of the real ServiceClient
public class ServiceClientFactory : IDisposable
{
    // This is the concrete implementation of IMyServiceClient 
    // that the factory will create and you can pass around; it 
    // provides both the reference count and facade implementation
    // and is nested inside the factory to indicate that consumers 
    // should not alter these (and cannot without reflecting on 
    // non-publics)
    private class CachedServiceClient : IMyServiceClient
    {
        internal ServiceClient _realServiceClient;
        internal int _referenceCount;

        #region Facade wrapper methods around real ServiceClient ones
        void IMyServiceClient.Query(string query)
        {
            _realServiceClient.Query(query);
        }
        #endregion

        #region IDisposable for the client facade
        private bool _isClientDisposed = false;

        protected virtual void Dispose(bool disposing)
        {
            if (!_isClientDisposed)
            {
                if (Interlocked.Decrement(ref _referenceCount) == 0)
                {
                    // if there are no more references, we really
                    // dispose the real object
                    using (_realServiceClient) { /*NOOP*/ }
                }
                _isClientDisposed = true;
            }
        }

        ~CachedServiceClient() { Dispose(false); }

        void IDisposable.Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
        #endregion
    }

    // The object cache; note that it is not static
    private readonly ConcurrentDictionary<string, CachedServiceClient> _cache
        = new ConcurrentDictionary<string, CachedServiceClient>();

    // The method which allows consumers to create the client; note
    // that it returns the facade interface, rather than the concrete
    // class, so as to hide the implementation details
    public IMyServiceClient CreateServiceClient(string host)
    {
        var cached = _cache.GetOrAdd(
            host,
            k => new CachedServiceClient()
        );
        if (Interlocked.Increment(ref cached._referenceCount) == 1)
        {
            cached._realServiceClient = new ServiceClient(host);
        }
        return cached;
    }

    #region IDisposable for the factory (will forcibly clean up all cached items)
    private bool _isFactoryDisposed = false;

    protected virtual void Dispose(bool disposing)
    {
        if (!_isFactoryDisposed)
        {
            Debug.WriteLine($"ServiceClientFactory #{GetHashCode()} disposing cache");
            if (disposing)
            {
                foreach (var element in _cache)
                {
                    element.Value._referenceCount = 0;
                    using (element.Value._realServiceClient) { }
                }
            }
            _cache.Clear();
            _isFactoryDisposed = true;
        }
    }

    ~ServiceClientFactory() { Dispose(false); }

    void IDisposable.Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
    #endregion
}

// This is just an example `ServiceClient` which uses the default
// implementation of GetHashCode to "prove" that new objects aren't
// created unnecessarily; note it does not implement `IMyServiceClient`
public class ServiceClient : IDisposable
{
    private readonly string _host;

    public ServiceClient(string host)
    {
        _host = host;
        Debug.WriteLine($"ServiceClient #{GetHashCode()} was created for {_host}");
    }

    public void Query(string query)
    {
        Debug.WriteLine($"ServiceClient #{GetHashCode()} asked '{query}' to {_host}");
    }

    public void Dispose()
    {
        Debug.WriteLine($"ServiceClient #{GetHashCode()} for {_host} was disposed");
        GC.SuppressFinalize(this);
    }
}

其次,一般来说,我会从工厂中删除static 子句并制作ServiceClientFactory : IDisposable。你会看到我在上面的例子中已经做到了。

您似乎只是将问题推到了链条上,但这样做可以让您根据具体情况做出决定,每个某事(应用程序,会话、请求、单元测试运行——任何有意义的东西)并让代表你的某物的对象负责处理。

如果您的 应用程序 将受益于单实例缓存,则将单例实例公开为 AppContext 类的一部分(例如)并在您的 clean-shutdown 例程中调用 AppContext.DefaultServiceClientFactory.Dispose()

这里的重点是彻底关闭。正如其他人所说,没有保证您的Dispose 方法实际上会被调用(想想电源循环机器,运行中)。因此,理想情况下,ServiceClient.Dispose 不会有任何有形副作用(即除了释放资源之外,如果进程终止或机器重新启动,自然会释放资源)。

如果ServiceClient.Dispose 确实有明显的副作用,那么您已经确定了这里的风险,并且您应该清楚地记录如何从“不干净”中恢复您的系统" 关机,在随附的用户手册中。

第三,如果ServiceClientQueryExecutor 对象都旨在可重用,则让消费者负责创建和处置。

QueryExecutor 在您的示例中只需要是IDisposable,因为它可以拥有ServiceClient(也是IDisposable)。如果 QueryExecutor 没有真正创建 ServiceClient,它不会负责销毁它。

相反,让构造函数采用ServiceClient 参数(或者,使用我的重写,一个IMyServiceClient 参数)而不是string,因此直接使用者可以对所有对象的生命周期负责:

using (var client = AppContext.DefaultServiceClientFactory.CreateServiceClient("localhost"))
{
    var query = new QueryExecutor(client);
    using (var reader = query.ExecuteReader("SELECT * FROM foo"))
    {
        //...
    }
}

PS:消费者是否需要实际直接访问ServiceClient,或者是否有其他需要引用它的对象?如果没有,也许可以稍微减少链并将这些东西直接移动到QueryExecutor,即使用QueryExecutorFactory 在缓存的ServiceClient 实例周围创建享元QueryExector 对象。

【讨论】:

    【解决方案2】:

    静态类在这种情况下很棘手。静态类的构造函数在第一次调用实例成员时被调用。如果应用程序域被销毁,该类将被销毁。如果应用程序突然终止(中止),则无法保证会调用构造函数/终结器。

    您最好的办法是重新设计类以使用单例模式。有办法解决这个问题,但是they require strange, dark magic forces that may cost you your soul.

    编辑: 正如 servy 指出的那样,Singleton 在这里无济于事。析构函数是终结器,您无法保证它会被调用(出于多种原因)。如果是我,我会简单地将其重构为实现 IDisposable 的可实例化类,并让调用者处理调用 Dispose。不理想,我知道,但这是唯一确定的方法。

    【讨论】:

    • 这个对象成为单例有什么帮助?
    • 单例是在其他地方实例化的非静态类。 (它甚至可以被实例化为静态类的属性,如果你愿意,可以保留。)然而,它的好处是它可以有一个析构函数,而静态类不能。
    • 它可以一个终结器,但它永远不会运行,因为总会有一个对它的根引用,所以它不会完成 任何事情,除了误导。
    • @Servy 我已经纠正了。早期的文档(大约 2010 年)对析构函数和终结函数进行了区分。最近的文档更清楚地表明它们是一回事。
    • @MikeHofer 请注意字典是静态的,但 ServiceClient 实例不是。造成问题的原因是 ServiceClient,而不是静态 Dictionary。
    猜你喜欢
    • 1970-01-01
    • 2012-02-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-07
    • 2014-09-24
    • 1970-01-01
    相关资源
    最近更新 更多