C# 有关Assembly.Unload详解

CLR产品单元经理(UnitManager)JasonZander在前几天一篇文章Whyisn'tthereanAssembly.Unloadmethod?中解释了为什么CLR中目前没有实现类似Win32API中UnloadLibrary函数功能的Assembly.Unload方法。

他认为之所以要实现Assembly.Unload函数,主要是为了回收空间和更新版本两类需求。前者在使用完Assembly后回收其占用资源,后者则卸载当前版本载入更新的版本。例如ASP.NET中对页面用到的Assembly程序的动态更新就是一个很好的使用示例。但如果提供了Assembly.Unload函数会引发一些问题:

1.为了包装CLR中代码所引用的代码地址都是有效的,必须跟踪诸如GC对象和COMCCW之类的特殊应用。否则会出现Unload一个Assembly后,还有CLR对象或COM组件使用到这个Assembly的代码或数据地址,进而导致访问异常。而为了避免这种错误进行的跟踪,目前是在AppDomain一级进行的,如果要加入Assembly.Unload支持,则跟踪的粒度必须降到Assembly一级,这虽然在技术上不是不能实现,但代价太大了。

2.如果支持Assembly.Unload则必须跟踪每个Assembly的代码使用到的句柄和对现有托管代码的引用。例如现在JITer在编译方法时,生成代码都在一个统一的区域,如果要支持卸载Assembly则必须对每个Assembly都进行独立编译。此外还有一些类似的资源使用问题,如果要分离跟踪技术上虽然可行,但代价较大,特别是在诸如WinCE这类资源有限的系统上问题比较明显。

3.CLR中支持跨AppDomain的Assembly载入优化,也就是domainneutral的优化,使得多个AppDomain可以共享一份代码,加快载入速度。而目前v1.0和v1.1无法处理卸载domainneutral类型代码。这也导致实现Assembly.Unload完整语义的困难性。

基于上述问题,JasonZander推荐使用其他的设计方法来回避对此功能的使用。如JunfengZhang在其BLog上介绍的AppDomainandShadowCopy,就是ASP.NET解决类似问题的方法。

在构造AppDomain时,通过AppDomain.CreateDomain方法的AppDomainSetup参数中AppDomainSetup.ShadowCopyFiles设置为"true"启用ShadowCopy策略;然后设置AppDomainSetup.ShadowCopyDirectories为复制目标目录;设置AppDomainSetup.CachePath+AppDomainSetup.ApplicationName指定缓存路径和文件名。

通过这种方法可以模拟Assembly.Unload的语义。实现上是将需要管理的Assembly载入到一个动态建立的AppDomain中,然后通过跨AppDomain的透明代理调用其功能,使用AppDomain.Unload实现Assembly.Unload语义的模拟。chornbe给出了一个简单的包装类,具体代码见文章末尾。

这样做虽然在语义上能够基本上模拟,但存在很多问题和代价:

1.性能:在CLR中,AppDomain是类似操作系统进程的逻辑概念,跨AppDomain通讯就跟以前跨进程通讯一样受到诸多限制。虽然通过透明代理对象能够实现类似跨进程COM对象调用的功能,自动完成参数的Marshaling操作,但必须付出相当的代价。DejanJelovic给出的例子(Cross-AppDomainCallsareExtremelySlow)中,P41.7G下只使用内建类型的调用大概需要1ms。这对于某些需要被频繁调用的函数来说代价实在太大了。如他提到实现一个绘图的插件,在OnPaint里面画200个点需要200ms的调用代价。虽然可以通过批量调用进行优化,但跨AppDomain调用效率的惩罚是肯定无法逃脱的。好在据说Whidbey中,对跨AppDomain调用中的内建类型,可以做不Marshal的优化,以至于达到比现有实现调用速度快7倍以上,...,我不知道该夸奖Whidbey实现的好呢,还是痛骂现有版本之烂,呵呵

2.易用性:需要单独卸载的Assembly中类型可能不支持Marshal,此时就需要自行处理类型的管理。

3.版本:在多个AppDomain中如何包装版本载入的正确性。

此外还有安全方面问题。对普通的Assembly.Load来说,载入的Assembly是运行在载入者的evidence下,而这绝对是一个安全隐患,可能遭受类似unix下面通过溢出以root权限读写文件的程序来改写系统文件的类似攻击。而单独在一个AppDomain中载入Assembly就能够单独设置CAS权限,降低执行权限。因为CLR架构下的四级权限控制机制,最细的粒度只能到AppDomain。好在据说Whidbey会加入对使用不同evidence载入Assembly的支持。

通过这些讨论可以看到,Assembly.Unload对于基于插件模型的程序来说,其语义的存在是很重要的。但在目前和近几个版本来说,通过AppDomain来模拟其语义是比较合适的选择,虽然要付出性能和易用性的问题,但能够更大程度上控制功能和安全性等方面因素。长远来说,Assembly.Unload的实现是完全可行的,Java中对类的卸载就是最好的例子,前面那些理由实际上都是工作量和复杂度方面的问题,并不存在无法解决的技术问题。

http://blogs.msdn.com/junfeng/archive/2004/02/09/69919.aspx#122956 re: AppDomain and Shadow Copy 4/30/2004 2:34 AM chornbe

You must also encapsulate the loaded assembly into another class, which is loaded by the new appdomain. Here's the code as it's working for me: (I've created a few custom exception types, and you'll notice I had them back - they're not descended from MarshalByRefObject so I can't just throw them from the encapsulated code)

using System;
using System.Reflection;
using System.Collections;

namespace Loader{

/* contains assembly loader objects, stored in a hash
* and keyed on the .dll file they represent. Each assembly loader
* object can be referenced by the original name/path and is used to
* load objects, returned as type Object. It is up to the calling class
* to cast the object to the necessary type for consumption.
* External interfaces are highly recommended!!
* */
public class ObjectLoader : IDisposable {

// essentially creates a parallel-hash pair setup
// one appDomain per loader
protected Hashtable domains = new Hashtable();
// one loader per assembly DLL
protected Hashtable loaders = new Hashtable();

public ObjectLoader() {/*...*/}

public object GetObject( string dllName, string typeName, object[] constructorParms ){
Loader.AssemblyLoader al = null;
object o = null;
try{
al = (Loader.AssemblyLoader)loaders[ dllName ];
} catch (Exception){}
if( al == null ){
AppDomainSetup setup = new AppDomainSetup();
setup.ShadowCopyFiles = "true";
AppDomain domain = AppDomain.CreateDomain( dllName, null, setup );
domains.Add( dllName, domain );
object[] parms = { dllName };
// object[] parms = null;
BindingFlags bindings = BindingFlags.CreateInstance | BindingFlags.Instance | BindingFlags.Public;
try{
al = (Loader.AssemblyLoader)domain.CreateInstanceFromAndUnwrap(
"Loader.dll", "Loader.AssemblyLoader", true, bindings, null, parms, null, null, null
);
} catch (Exception){
throw new AssemblyLoadFailureException();
}
if( al != null ){
if( !loaders.ContainsKey( dllName ) ){
loaders.Add( dllName, al );
} else {
throw new AssemblyAlreadyLoadedException();
}
} else {
throw new AssemblyNotLoadedException();
}
}
if( al != null ){
o = al.GetObject( typeName, constructorParms );
if( o != null && o is AssemblyNotLoadedException ){
throw new AssemblyNotLoadedException();
}
if( o == null || o is ObjectLoadFailureException ){
string msg = "Object could not be loaded. Check that type name " + typeName +
" and constructor parameters are correct. Ensure that type name " + typeName +
" exists in the assembly " + dllName + ".";
throw new ObjectLoadFailureException( msg );
}
}
return o;
}

public void Unload( string dllName ){
if( domains.ContainsKey( dllName ) ){
AppDomain domain = (AppDomain)domains[ dllName ];
AppDomain.Unload( domain );
domains.Remove( dllName );
}
}

~ObjectLoader(){
dispose( false );
}

public void Dispose(){
dispose( true );
}

private void dispose( bool disposing ){
if( disposing ){
loaders.Clear();
foreach( object o in domains.Keys ){
string dllName = o.ToString();
Unload( dllName );
}
domains.Clear();
}
}
}

}

--- end cut

--- cut second class file
using System;
using System.Reflection;

namespace Loader {
// container for assembly and exposes a GetObject function
// to create a late-bound object for casting by the consumer
// this class is meant to be contained in a separate appDomain
// controlled by ObjectLoader class to allow for proper encapsulation
// which enables proper shadow-copying functionality.
internal class AssemblyLoader : MarshalByRefObject, IDisposable {

#region class-level declarations
private Assembly a = null;
#endregion

#region constructors and destructors
public AssemblyLoader( string fullPath ){
if( a == null ){
a = Assembly.LoadFrom( fullPath );
}
}
~AssemblyLoader(){
dispose( false );
}

public void Dispose(){
dispose( true );
}

private void dispose( bool disposing ){
if( disposing ){
a = null;
System.GC.Collect();
System.GC.WaitForPendingFinalizers();
System.GC.Collect( 0 );
}
}
#endregion

#region public functionality
public object GetObject( string typename, object[] ctorParms ){
BindingFlags flags = BindingFlags.CreateInstance | BindingFlags.Instance | BindingFlags.Public;
object o = null;
if( a != null ){
try{
o = a.CreateInstance( typename, true, flags, null, ctorParms, null, null );
} catch (Exception){
o = new ObjectLoadFailureException();
}
} else {
o = new AssemblyNotLoadedException();
}
return o;
}
public object GetObject( string typename ){
return GetObject( typename, null );
}
#endregion

}
}
收藏 (0)
评论列表
正在载入评论列表...
我是有底线的
为您推荐
    暂时没有数据