如果你是一个IOC新手,那么生命周期可能会比较难以理解。以至于谈到这个问题时,一些老手也时常表示疑虑和害怕。一个令人不安的问题就是-对象没有在合适的时机被销毁。这样一来内存的使用率就会一直攀升,直到程序抛出OutOfMemoryException异常。(如果不熟悉生命周期这样的问题可能一直咬着我们不放)
如何避免在使用IOC容器时的生命周期问题,其实也不是特别复杂。并不仅仅是IOC容器的API使用方面,更多的是因为你的应用程序设计问题。以下将有一些好的建议,个人感觉很少有人从头到尾讲解这个问题,今天我将用这篇相文章来做这件事,同时也希望大家能够即时讨论,以供其他人员作为参考。 这篇文章是关于Autofac的,但其实这是一个很普遍的主题–即使你还不是一个autofac使用者,这也应可以帮助你做出容器选择。
让我们从下面的问题开始。
可销毁的对象,通常我们是指实现了“IDisposable interface”的任何组件对象,如下示例:
interface IMyResource { … }
class MyResource : IMyResource, IDisposable { … }
当在autofac容器中创建myresource实例时,容器将保持对它的引用,即使它不再被其他程序使用。这意味着垃圾收集器将无法收回该组件所持有的内存,因此下面的程序最终将耗尽所有可用的内存和崩溃:
var builder = new ContainerBuilder();
builder.RegisterType<MyResource>().As<IMyResource>();
using (var container = builder.Build())
{
while (true)
var r = container.Resolve<IMyResource>(); // Out of memory!
}
这与在CLR中典型的创建对象行为大相径庭,如果我们直接在循环中创建的myresource对象,根本就不会有任何问题,如下:
while (true)
var r = new MyResource(); // 很好,GC处理的漂亮。
过渡依赖
看看上面的代码,您可能会想,只有从容器中直接解析组件时会出现这个问题,然而这只是个表面问题。事实并非如此——容器创建的每一个组件都会被跟踪,甚至包括那些间接创建的依赖关系组件。
interface IMyService { }
class MyComponent : IMyService
{
// Dependency on a service provided by a disposable component
public MyComponent(IMyResource resource) { … }
}
如下IMyService组件的解析依赖于另一个可销毁组件(IMyResource ),则内存泄漏仍然存在:
while (true)
// Still holds instances of MyResource
var s = container.Resolve<IMyService>();
从委托工厂返回的结果
有时候我们可能需要使用一个autofac代理工厂类型比如Func<T>,而不是直接在接在容器上调用resolve():
interface IMyService2
{
void Go();
}
class MyComponent2 : IMyService2
{
Func _resourceFactory;
public MyComponent(Func<IMyResource> resourceFactory)
{
_resourceFactory = resourceFactory;
}
public void Go()
{
while (true)
var r = _resourceFactory(); // Still out of memory.
}
}
现在在这个循环中我们只解析一个组件的实例并调用go()方法。
using (var container = builder.Build())
{
var s = container.Resolve<IMyService2>();
s.Go();
}
此时尽管我们只是直接调用了容器方法一次,但泄漏仍然存在。
autofac为什么会出现这样的情况?
在这里似乎有几个陷阱,但是只有一条值得重申: 不管该实例是如何被请求的,autofac将跟踪它创造每一个组件实例。
Autofac进行了非常仔细地设计,即使没有容器的编程,资源管理也非常容易。注意这里讲到的”资源管理“,通常为了防止”内存泄漏“的方法是通过IDisposable来管理“资源”
什么是资源?
一般来讲,资源可以定义为具有有限容量的任何必须在其用户之间共享的资源。
在IOC容器中的组件的上下文中,资源是在其生命周期中获得和释放的任何阶段。下面有有一些低级别的示例,例如依赖于下面列表中的项的组件,或者具有类似语义的高级应用程序结构也比较常见:
- 锁(例如监视器。enter()和监控。exit())
- 事务(开始和提交/中止)
- 事件订阅(+ =和=)
- 定时器(启动和处理)
- 机的资源如套接字和文件(通常是打开/创建和关闭)
- 等待工线程(创建和信号)
NET中有一种表示资源语义的标准方法。就是通过实现IDisposable接口的组件,当不再需要这样的组件时,通过调用dispose()方法来完成“释放”阶段。
不调用dispose()是一个最常见的问题
你可以通过Kim Hamilton和Joe Duffy的文章阅读一些有趣的讨论。其中讨论的主题是dispose()必须要调用。在这里有一个很强烈的共识:未能调用dispose()将导致一个错误,不管是否有问题的资源是由一个终结器保护(或SafeHandle)。
在IOC(假设你现在用它)之前,大概有两种方法可以调用dispose():
1.C #using声明
2.Ad Hoc
IDisposable和using,是天造地设的一对,但他们只适用于当一个资源的生命周期在一个方法调用中时才适用。
对于其他复杂情况,您需要找到一种策略,以确保资源不再需要时进行处理。最广泛的尝试是基于这样一个想法:任何对象获取资源都应该释放它。我们轻蔑地称之为“自组织”,因为它不能持续的工作。最终你会遇到下列问题中的一个(可能更多):
1.共享:当多个独立组件共享一个资源时,很难确定它们中没有一个需要它。要么第三方必须了解资源的所有潜在用户,否则用户将不得不合作。不管怎样,事情很快就会变的很快。
2.级联变化:假设我们有三个组件——a使用b,它使用C。如果没有涉及到资源,那么就不需要考虑资源所有权或发布的工作原理。但是,如果应用程序改变了,C必须拥有一个可弃资源,那么A和B可能不得不在不再需要该资源时适当地改变(通过处理)信号。更多的成分,这是很讨厌的。
3.合同与执行:在.NET中,我们鼓励对一个合同进行计划,而不是一个实现。设计良好的API通常不提供实现类型的详细信息,例如可以使用抽象工厂来创建不同大小的缓存:
public ICache CreateCache(int maximumByteSize); // Abstract Factory
interface ICache // : IDisposable?
{
void Add(object key, object value, TimeSpan ttl);
bool TryLookup(object key, out object value);
}
实施初期可能只返回memorycache对象,没有资源管理的要求。但是,因为我们在未来可能会创造一个filecache实施,这意味着内容应该是一次性的吗?
问题的根源在于,对于任何contract,可以想象,总有一天需要一个可释放的实现。
这种特殊的问题被像IOC这样的松散耦合系统所加剧。由于组件只通过契约(服务)相互了解,而从来没有实现类型,所以一个组件不能确定它是否应该处理另一个组件。
用IOC来解决上面的问题!
有一个可行的解决方案。正如您所猜测的,在典型的企业和Web应用程序和B)中,在一定的粒度级别下,IOC容器为资源管理问题提供了一个很好的解决方案。
要做到这一点,他们需要掌握他们创造的一次性组件。
但这只是故事的一部分——他们还需要被告知应用程序执行的工作单元。这就是容器将知道如何处理组件和释放引用,避免出现内存泄漏的问题。
为什么不使用WeakReference容器才是安全的?一个依靠WeakReference溶液被同样的问题dispose()都不给。允许组件在封闭的工作单元完成之前进行垃圾收集,会导致一系列微妙的负载和环境相关的bug。许多更高级别的资源也无法在一个适当的析构函数释放。
Units of Work
当它们运行时,企业和Web应用程序倾向于执行具有定义的开始和结束的任务。这些任务可能是响应Web请求、处理传入消息或在某些数据上运行批处理过程之类的事情。
这些都是抽象意义上的任务——不要与任务或任何异步编程构造混淆。为了使这一点更清楚,我们将选择术语“工作单元”来描述这种任务。
确定工作单元的开始和结束,是在应用程序中有效地使用autofac关键。
生命周期:在autofac中实现Units of Work
autofac通过lifetime scopes来满足units of work,一个生命周期范围(ilifetimescope)听起来像–哪一个scope完成了,那么它相关的组件的的也将结束。
处理工作单元的时候,已经解析的实例组件取得与生命周期的关联。通过跟踪实例化组件的顺序并确保只能在相同或更长的生命周期范围内满足组件的依赖关系。当寿命范围结束autofac可以负责处理。
回到我们最初的微不足道的例子,我们观察到的漏洞可以通过创建和处理一个新的生命周期范围来消除:
// var container = …
while (true)
{
using (var lifetimeScope = container.BeginLifetimeScope())
{
var r = lifetimeScope.Resolve<IMyResource>();
// r, all of its dependencies and any other components
// created indirectly will be released here
}
}
使用autofac可以避免所有与内存和资源泄漏。
请不要从根容器中解析实例组件。而应该总是从生命周期范围中解析然后释放。
Lifetime scopes是非常灵活的,可以应用到大量的场景中。有一些很有用的东西可能不会立即显现出来。
多个lifetime scope可以同时存在于同一容器
下图展示如何传入HTTP请求并在autofac ASP.NET集成中进行处理,如图:对于每个http请求autofac都会产生一个新的 lifetime scope来处理它;然后在请求结束时会得到清理
注意,我们将生命周期范围层次结构的根称为“应用程序”范围,因为它将在应用程序运行期间持续生存。
lifetime scope可以嵌套
在autofac应用中,容器本身是一个根lifetime scope。当从它创建一个lifetime scope时,这将在嵌套作用域和容器之间建立parent-child关系。
当依赖解析发生,Autofac首先试图满足已经建立请求的那个范围内的实例组件依赖。在下图对于下图来讲,这意味着:依赖关系,在HTTP请求和会话范围内,NHibernate会话将被满足,在HTTP请求范围结束时将被清理。
当解析操作从子范围移动到父进程时, 任何进一步的依赖将在上级范围内解析。如果singleinstance()组件日志取决于LogFile将日志文件的依赖将与根范围有关,所以即使当前HTTP请求范围结束,文件组件是否存在将取决于LOG
Lifetime scopes可以嵌套任意深度:
var ls1 = container.BeginLifetimeScope();
var ls2 = ls1.BeginLifetimeScope();
// ls1 is a child of the container, ls2 is a child of ls1
共享是由Lifetime Scopes驱动的
在这节我们讨论下生命周期的管理,其中值得一提的是lifetime scopes和Autofac实现共享之间的关系。
默认情况下,一个组件的实例如果是必要的(任何一个直接调用了resolve()方法或者作为一个依赖另一个组件)autofac都将创建这个组件的一个新的实例,如果它是可以清除的,就将其附加到当前lifetime scope。 我所知道的所有IOC容器都支持这种“transient lifestyle”或“instance per dependency”之类的东西。同样普遍支持的是“singleton”或“single instance”共享,这其中的single实例用于满足所有的请求。在Autofac中singleInstance() sharing将一个实例与当前active的lifetime scopes的树的根节点,即容器建立联系。
这里有两个autofac常用的共享方式。
(1) 匹配范围共享
创建scope时,可以 以“标记”的形式给它一个名称:
// For each session concurrently managed by the application
var sessionScope = container.BeginLifetimeScope("session");
// For each message received in the session:对于会话中接收到的每个消息
var messageScope = sessionScope.BeginLifetimeScope("message");
var dispatcher = messageScope.Resolve<IMessageDispatcher>();
dispatcher.Dispatch(…);
在lifetime scope的层次结构中标记命名级别,并且在注册组件时可以使用它们来确定实例是如何共享的。
builder.RegisterType<CredentialCache>()
.InstancePerMatchingLifetimeScope("session");
在这种情况下,当处理消息的会话中,即使我们始终从messagescopes resolve()依赖,所有的组件将在会话级共享一个CredentialCache。
(2)当前范围共享
虽然偶尔的units of work被嵌套成多个层,但大多数情况下一个可执行程序只处理一种工作单元。
在一个Web应用程序中,为每个HTTP请求和可能被标记为“HttpRequest”从root上创建了一个子scopes 。在后台的处理中,我们也可以有一个2级范围的层次,但子范围可能 per-"workItem".。
有许多类型的组件,比如与持久性或缓存相关的组件,需要在每个unit of work中共享,不管组件使用的是哪个应用程序模型。这类组件,autofac提供了InstancePerLifetimeScope()这个共享模型。
如果一个组件被配置为使用一个InstancePerlifetimeScope(),该组件将在同一个lifetime的所有其他组件之间共享最多一个实例,而不管它们所涉及的范围的级别或标签。这对于在同一解决方案中重用不同应用程序或服务之间的组件和配置非常有用。
Lifetime Scopes没有任何上下文依赖
在同一线程上创建多个独立的生命周期范围是完全可以接受的:
var ls1 = container.BeginLifetimeScope();
var ls2 = container.BeginLifetimeScope();
// ls1 and ls2 are completely independent of each other
在许多线程之间共享一个生命周期范围也是完全安全的,并在线程以外的线程上结束一个生命周期作用域。
生命周期范围不依赖于线程本地存储或类似HTTP上下文的全局状态来完成它们的工作。
组件可以创建Lifetime Scopes
容器可以直接用于创建生命周期范围,但大多数情况下您不希望为此使用全局容器变量。
所有的组件需要创建和使用寿命范围是要依赖ilifetimescope接口:
class MessageDispatcher : IMessageDispatcher
{
ILifetimeScope _lifetimeScope;
public MessageDispatcher(ILifetimeScope lifetimeScope)
{
_lifetimeScope = lifetimeScope;
}
public void OnMessage(object message)
{
using (var messageScope = _lifetimeScope.BeginLifetimeScope())
{
var handler = messageScope.Resolve<IMessageHandler>();
handler.Handle(message);
}
}
}
ILifetimeScope实例注入的将是组件的lives的其中之一。