monodis是mono发行包里的一个工具,作用类似与ms的ildasm,可以把dotnet pe文件反编译为msil文件(另外有个托管代码的实现Mono.Cecil)。这个工具的实现很简单,就是根据PE文件的格式与规范去解析。选择这个主题的原因有很多,首先PE文件作为进行分析mono的基础,毕竟这里是metadata的来源;另外通过分析msil语言,可以为后续的VM执行引擎做准备,毕竟无论是jit还是aot,都是从msil到x86代码的转换,msil是第一步;当然了解这个还可以为很多其他项目的分析做准备,比如最近我脱离reflector很喜欢的一个工具ilspy,是把msil做ast分析转换为C#。好了,简介到此为止,下面来具体分析下这个工具的源码实现。

一.文件结构及依赖

monodis在mono/dis目录下,通过查看makefile可见mono依赖于libmono-$(API_VER).la,这部分功能主要和mono共用metadata解析部分的代码,除了这些monodis自身的文件包括:
1.main.c:这个主要是提供参数解析及程序的入口,有参数可见monodis可以提供PE文件各部分信息的单独输出。
2.dump.c:这个是main.c里输出各种table直接调用的方法,比如assembly,typeref,class,method,filed,event等等我们非常关心的一些东西。
3.get.c:主要提供一些stringfy的方法,即把MonoImage,MonoMethodSignature,MonoGenericContainer等Mono metadata相关的数据结构表示为disassemble后的字符串。
4.dis-cil.c:这个可能是我们最关心的方法了,因为它的作用就是把MonoMethodHeader输出为msil代码。
5.util.c:这个文件提供了四个工具方法:map和flags是查表方法,根据传入的32位值得到相应的字符串标记,还有hex_dump和datadump,这两个是把data直接输出的。

 二.反编译的过程

1.加载file,解释为assembly:

这一块功能主要是由libmono实现的,具体代码在image.c/do_mono_image_load函数中, 这一块是和mono共用的,我们这里所关心的就是load_metadata所调用的load_tables方法,因为MonoImage->tables是后续所有解析的来源,具体这一块的实现这里不详细的讲,因为这个不是本文的重点。

2.由上一步的工作得到了monodis反编译过程中需要的MonoImage结构体数据,下面就进行了如下一系列的工作:

dump_header_data (img);
dis_directive_assemblyref (img);
dis_directive_assembly (img);
dis_directive_file (img);
dis_directive_mresource (img);
dis_directive_module (img);
dis_directive_moduleref (img);
dis_exported_types (img);
dis_nt_header (img);
dis_mresource (img);
dis_types (img, 
0);
dis_data (img);

 由函数名即可得知,这是处理各个table呢。由于其中大部分工作都类似,所有这里只选择分析其中最具代表性的dis_directive_assemblyref方法和最重要的distypes这个方法。 

2.1:下面来着重讲一下这个通用的工作流程,也就是以上方法通用的解析步骤,以dis_directive_assemblyref为例:

static void
dis_directive_assemblyref (MonoImage 
*m)
{
    MonoTableInfo 
*= &m->tables [MONO_TABLE_ASSEMBLYREF];
    guint32 cols [MONO_ASSEMBLYREF_SIZE];
    
int i;
    
    
if (t->base == NULL)
        
return;

    
for (i = 0; i < t->rows; i++){
        
char *esc, *flags;

        mono_metadata_decode_row (t, i, cols, MONO_ASSEMBLYREF_SIZE);

        esc 
= get_escaped_name (mono_metadata_string_heap (m, cols [MONO_ASSEMBLYREF_NAME]));
        flags 
= assembly_flags (cols [MONO_ASSEMBLYREF_FLAGS]);
        
        fprintf (output,
             
".assembly extern %s%s\n"
             
"{\n"
             
"  .ver %d:%d:%d:%d\n",
             flags,
             esc,
             cols [MONO_ASSEMBLYREF_MAJOR_VERSION], cols [MONO_ASSEMBLYREF_MINOR_VERSION], 
             cols [MONO_ASSEMBLYREF_BUILD_NUMBER], cols [MONO_ASSEMBLYREF_REV_NUMBER]
            );
        dump_cattrs (m, MONO_TOKEN_ASSEMBLY_REF 
| (i + 1), "  ");
        
if (cols [MONO_ASSEMBLYREF_CULTURE]){
            fprintf (output, 
"  .locale %s\n", mono_metadata_string_heap (m, cols [MONO_ASSEMBLYREF_CULTURE]));
        }
        
if (cols [MONO_ASSEMBLYREF_PUBLIC_KEY]){
            
const char* b = mono_metadata_blob_heap (m, cols [MONO_ASSEMBLYREF_PUBLIC_KEY]);
            
int len = mono_metadata_decode_blob_size (b, &b);
            
char *dump = data_dump (b, len, "\t\t");
            fprintf (output, 
"  .publickeytoken =%s", dump);
            g_free (dump);
        }
        fprintf (output, 
"}\n");
        g_free (flags);
        g_free (esc);
    }

首先从m->tables取得相应的table得到MonoTableInfo,具体这个数据是从哪里来的,是第一步所做的工作,得到tableinfo之后,会对其进行遍历,然后用mono_metadata_decode_row解析到定义过的col变量里。这个col的大小及每个字段的定义都在row-indexes.h定义的各种枚举里面,至于为什么以及每个字段的含义,请直接参考PE文件格式,另外有本书写的也非常好《加密与解密》。取得col数据之后,就是把这些数据根据msil的文件格式要求输出,如上例以及我测试用的一个PE文件输出为:
.assembly extern mscorlib
{
  .ver 
2:0:0:0
  .publickeytoken 
= (B7 7A 5C 56 19 34 E0 89 ) // .z\V.4..

 其他的一些table输出方式与上面一样,只是格式不同而已。

2.2:现在来分析其中最重要的也是我们最关心的distype方法。这里其实有几个重要的部分,依次进行分析:

(1).输出type的基本信息,包括name,namespace以及type相关的attribute,这部分逻辑是通用的,和dis_directive_assemblyref一样的方式。

(2).输出field,method,property,event等type的成员。分别对应dis_field_list,dis_method_list,dis_property_list,dis_event_list方法。这里只分析其中典型的dis_method_list方法其中的dis_code方法。

首先由mono_image_rva_map获取一个方法的偏移量指针,然后再由mono_metadata_parse_mh_full得到这个方法的MonoMethodHeader。以下是其定义:

 

struct _MonoMethodHeader {
    
const unsigned char  *code;
#ifdef MONO_SMALL_CONFIG
    guint16      code_size;
#else
    guint32      code_size;
#endif
    guint16      max_stack   : 
15;
    unsigned 
int is_transient: 1/* mono_metadata_free_mh () will actually free this header */
    unsigned 
int num_clauses : 15;
    
/* if num_locals != 0, then the following apply: */
    unsigned 
int init_locals : 1;
    guint16      num_locals;
    MonoExceptionClause 
*clauses;
    MonoType    
*locals [MONO_ZERO_LEN_ARRAY];
};

 

如果看着这个结构体特别亲切,那就对了。这个就是msil中的method的一个抽象,比如locals,exception clause,code等。

其实对_MonoMethodHeader的输出,就是对这个结构的一个stringfy。当然由code到msil还需要一个过程,那就是disassemble_cil方法所做的工作了。这个过程就是把code根据其对应的opcode进行翻译,解释为我们平常熟悉的msil了。这个翻译的过程很简单,具体代码在dis-cil.c中的disassemble_cil方法,其中需要注意的一点就是会根据每个opcode的argument不同而翻译为不同的表示。


好了,这就是monodis的一些关键部分,当然还有很多没有解释也没有必要去解释,因为都是类似的操作,把这个过程搞清楚则对metadata,image,msil等一些基础性的概念有更深的了解,也是进一步分析mono的基础。

 

posted @ 2011-04-23 16:41 zffl 阅读(1105) 评论(0) 编辑
本文目的很简单,简单分析一下下面语句是如何被Mono Runtime执行的:  
public class Hello1
{
   public static void Main()
   {
      System.Console.WriteLine("Hello, World!");
   }
}
  由于自己也是刚接触Mono源码,以前一直停留于简单的应用之上,所以本文的分析不全面,只是为了能大致的了解Mono的执行流程,对基本的函数和流程有所了解,所以本文的风格为流水帐。

一、先简单的看下Mono源码的目录中的几个重要文件夹:

我这里使用的是不知道什么时候下载的某个历史版本2.6.7,目前我觉得重要的就3个文件夹,mcs为base framework这里除了常见的System namespace下的各种重要程序集之外还有以Mono作为namespace的一些程序集,听雨痕说玩Mono不能错过这些程序集,有空看下;libgc文件夹里貌似是提供GC的支持库;最重要一个文件夹就是mono了,下面着重看下这个目录,这里实现的是Mono Runtime,本文调试的代码也主要是这里的。

1.arch:此目录放了各种不同处理器的差异代码,看下x86里面就两个文件x86-codegen.h和tramp.c,这个tramp.c通过调用x86-codegen.h里的各种奇怪的宏提供了两个方法mono_arch_create_method_pointer和mono_arch_create_trampoline,依这个名字看是由MonoMethod *method生成一个本机代码的指针。。。不过这两个方法太过抽象还没看懂,先放到一边。

2.io-layer:从这个目录下的各个文件名来看,这个是和操作系统打交道的,比如posix,socket,threads,semaphores,io等等,这个我很感兴趣,特别是P/Invoke是如何和这些打交道的,这一块在以后也可以做深入的分析。

3.metadata:这一块是实现cli的,比如appdomain,assembly,class&object,cominterop,exception等等,不过还有gc,coree,filewatcher等等,看来这里不只是cli的实现,还有一堆其他的东西。

4.mini:这个名字起的很奇怪,为啥叫mini呢?我猜可能是为了和mono对应吧。。。其实这里是Mono Runtime里最基础的部分,比如aotLinear IR的jit,debugger等等,当然main函数也再这里。

5.utils和其他:utils提供一些基础设施方面的功能,比如内存分配方面的,常见的数据结构实现,以及各种工具方法。其他的几个文件夹我暂时没发现有价值的东西,先搁在一边。

二、切入正题,打断点分析源码

1.第一步尝试:找出编译的步骤和执行的入口,单步来分析执行流程。 在mono_main() at driver.c里找到了main_thread_handler方法,这个是mono在解析完一系列args后的调用,这个方法是入口,接着往下看,由于没有使用mono_compile_aot,所以直接使用mono_domain_assembly_open打开程序集,执行mono_jit_exec。。。。具体的细节就不再写下去了,因为我发现这样写分析的方法没有人能看懂,我自己都懒得看。但是结果得说一下,通过这种方式我最终也没有找到再哪点输出了Hello World!把我记的笔记贴出来,换一种方法  
mono_main:driver.c
main_thread_handler:driver.c
mono_jit_exec :driver.c
mono_runtime_run_main:object.c
mono_runtime_exec_main:object.c
mono_runtime_invoke:mini.c
runtime_invoke:指针。
mono_custom_attrs_from_method:reflection.c,原来是取是否要线程安全的。
mono_jit_compile_method_with_opt:mini.c
mono_jit_compile_method_inner:MONO_USE_AOT_COMPILER
mono_compile_create_vars这个创建私有作用域的变量吗?
mono_method_get_header获取_MonoMethodHeader是做什么的?
mono_arch_create_vars指定特定arch的操作。
mono_method_to_ir:mono_method_to_ir.c这个好生疏阿。Translate the .net IL into linear IR
mono_method_to_ir是一个很重要的方法,在这里实现了il的解析。
mono_optimize_branches:优化分支,开始执行代码优化了,编译理论书上写的没错。

  2.第二次尝试:找到在哪执行了向stdout上写hello world! 这一次直接一点,直接去找最终的执行函数,中间的无数步骤先忽略。最初我再源码里翻出了console-io.h这个头文件,发现这里有操作console的,然后就断点阿,每个相关的函数都断上,执行的最后只有初始化函数命中了,尝试失败。那么到底再哪点往stdout写数据呢?我再次尝试一种方法,在执行时加上--trace选项(即再terminal里执行mono --trace hello.exe >> hello.trace.out),把执行流程重定向的一个文本文件中,输出了700多行,恩,我想从这里找出点线索。

1.首先在hello.trace.out文件里搜索Hello, World!字符串,找到几个匹配的地方,其中有一个是 [0xb75616f0: 0.02866 1] ENTER: System.Console:WriteLine (string)([STRING:0x53fa0:Hello, World!], ) 从这个trace的结果上可以看出,[]包括的是地址和时间,后面跟着ENTER:,于是我就再代码里搜索ENTER:,找到/mono/mini/trace.c的342行,加上条件断点:fname = "System.Console:WriteLine",执行一下,ok,命中断点,堆栈信息如下。  

Thread [1] 8643 (Suspended : Breakpoint)
    mono_trace_enter_method() at trace.c:342 0x80dbe6c
    0xfb817c
    0xfb8225
    0xfb80f8
    mono_jit_runtime_invoke() at mini.c:4,789 0x8065cc1
    mono_runtime_invoke() at object.c:2,613 0x8195d35
    create_exception_two_strings() at exception.c:133 0x81e6a9f
    mono_exception_from_name_two_strings() at exception.c:156 0x81e6adf
    create_exceptions() at appdomain.c:185 0x818b9e4
    mono_runtime_init() at appdomain.c:262 0x818bc0d
    mini_init() at mini.c:5,520 0x806734a
    mono_main() at driver.c:1,623 0x80ec127
    main() at main.c:34 0x805b950
  再看看后面几个的调用
System.IO.SynchronizedWriter:WriteLine
System.IO.TextWriter:WriteLine
System.IO.UnexceptionalStreamWriter:Write
System.IO.StreamWriter:Write
。。。。。
System.IO.MonoIO:Write

都是同样的调用堆栈,另外有关于这些System.Console相关的函数都在mono-2.6.7/mcs/class/corlib/System/Console.cs里,这个代码里有MonoLimitation和#if NET_2_1 && !MONOTOUCH部分符号,有点意思。

2.现在可以大致确定每个clr的方法调用都是由mono_jit_runtime_invoke发出的,但是我有点不解的是有三个函数没有symbol,直接显示的是函数地址,这给我的进一步分析带来了难度,来看看是为什么。执行函数返回退回到堆栈mono_jit_runtime_invoke,可以看到那几个没有符号信息的地址是由runtime_invoke这个函数执行的。转到定义,可见  

MonoObject *(*runtime_invoke) (MonoObject *this, void **params, MonoObject **exc, void* compiled_method);
......
if (!info->dyn_call_info)
    info->runtime_invoke = mono_jit_compile_method (invoke);
......
runtime_invoke = info->runtime_invoke;
......
return runtime_invoke (obj, params, exc, info->compiled_method);

  可见runtime_invoke是由mono_jit_compile_method这个方法进行jit生成的代码,怪不得找不到符号了。

3.继续深入到mono_jit_compile_method_with_opt这个动态代码生成的函数,关于jit方面的,留下次分析吧。

独立博客地址(需翻墙):http://i.zffl.us/?p=5001

posted @ 2011-03-13 21:43 zffl 阅读(1608) 评论(8) 编辑
这是前两周在部门里做的nodejs分享,可能是由于第一次,没有分享的经验,所以在结束之后感觉讲的不太好。最直接的感受就是最后demo演示和社区讨论的部分比前面分析nodejs是如何实现的分析更受大家关注,这个顺序被我搞错了,开始就和大家讲require是怎么实现的,socket,file对象在javascript中是如何实现的等等这些抽象的主题,在分享之前真没有想到这些问题,总之是一次宝贵的经验。
Nodejs部门分享
View more presentations from zffl.

独立博客地址(需翻墙):http://i.zffl.us/?p=1 

 

posted @ 2011-03-13 21:39 zffl 阅读(127) 评论(0) 编辑

我们已经在Part V产生了中间代码,并且我们想要把它转换成可执行代码,好让我们能够执行一个程序.但是我已经决定要先建立一个虚拟机,这样我们可以知道该如何处理产生可 执行代码.

虚拟机当然是一个脚本引擎中非常重要的组件.我们的代码将在它那里执行,所以它最好快一些.但是这里 我将不把焦点集中到速度上.

Oh yeah:这部分结束后,你将完全免费的得到我那令人惊奇的堆栈模板(Amazing Stack Template),也不需要额外的小费.并且你将得到一个为这部分特别编写的很酷的字符串类,它完成至少5个精密的工作.那是你的物有所值的东西.

但是,首先是一个不同机器类型的说明.在Part V我只是说了我们的VM将是什么种类,没有说明其它的可能.Andy Campbell询问我关于这方面的其它可能性,并且我想其他人也许会感兴趣.

机器类型
gl_line.jpg (1418 bytes)

以前说过,我们的机器将是一个堆栈机器(stack machine).在真实的机器中,堆栈CPU被用于早期的计算机(并且今天依然在一些简单的设备中使用).缺点是需要很多的堆栈操作:每个操作数需要一 个PUSH,每个结果需要一个POP.尽管你直接使用这个结果来进行下面的计算,所以那不总是必须的.

现在的大多数CPU有寄存器(数量非常有限的存储位置)来进行操作而不是堆栈;堆栈依然在函数传递参 数时使用.可以只在寄存器上操作的机器被称为load/store机器,因为你必须load每个你用到的值,然后在你计算完后store每个结果.

某些处理器只操作内存数据;没有堆栈,也没有寄存器.使用这种处理器的机器被称为三地址机器 (three-address machines),因为绝大多数指令有三个地址操作数(例如 ADD dest,src1,src2).由于内存带宽的限制,我认为他们不会在很多硬件中使用,但是他是虚拟机的一个选择.

对于虚拟机,堆栈机器非常容易实现,因为当你计算一个表达式时不需要临时变量来存储中间结果;你把所 有东西放入堆栈(它与你处理一个后缀表达式的方法十分相似).虽然我将在这里使用临时变量.后面还有更多内容.

我不清楚三地址机器是否可能有一个优点;速度是最重要的一个,尽管我尝试了两者,我能肯定的说出哪个 在优化中做了更少的计算...我想优化三地址代码更容易,所以也许这是这种机器的一个优点.

JAVA表面上使用一个堆栈机器(我听说是这样,我对JAVA VM不熟).

一件虚拟的非常容易的事(A Virtual Piece of Cake)
bl_line.jpg (1610 bytes)

我们的虚拟机对象根本就不复杂.它的最重要的成员有:一个指令数组,一个字符串表和一个堆栈.它有三 个主要的接口函数:Reset,Read和Execute.

指令数组存储我们的程序包含的指令.指令类简单极了,看上去就像我们在Part 5中的中间代码使用的一样.

字符串表只是一个指针数组,它可以是NULL或者一个当前使用的字符串.这可能是一个程序的变量,或 者一个堆栈中的临时变量.

我们的堆栈是由整数组成的.它们指向字符串表,使我们知道什么字符串现在在堆栈中.为什么我使用整 数,而不是字符串类的指针呢?因为我想保持事物的简单(为了读者,也为了我自己):记住我们有时也想让堆栈存储布尔值,所以我们不得不建立一个存储字符串 指针或布尔值的'stack item'类...现在我们只是使用一个整数:如果它是非负数,我们知道它指向一个字符串,如果它是负数它就是一个布尔值.它是脏的代码,但是他有利于工 作并且每个人都可以理解它.不要在家试它,不要在一个真正的项目中使用它.

现在是接口函数.'Reset'重新初始化VM.它是一个很简单的函数.

'Read'将要在程序中读取.下次我们将改变这个函数让他从stdin中读取,但是现在它里面有一 个测试程序.如果你喜欢就改写它--只是小心的让程序保持正确,不要让我们的VM崩溃.

'Execute'执行当前在内存中的程序.这也是一个简单的函数:它有一个指令指针,它察看一个指 令,然后使用一个switch语句执行正确的代码.关于临时变量的一个说明:每当我们把一个变量放到堆栈,我们需要它的一个拷贝:我们不能只是把在字符串 表中的变量的索引值进栈,因为他们的值可能改变并且接着堆栈中的值也会改变.这就是为什么几乎每个堆栈操作都使用NewTempCopy和 DelTempCopy.

一点关于优化VM的说明:我们应该确保我们的堆栈操作尽可能的快;我们的堆栈模板不是特别的快.在字 符串操作上也一样.一般而言,我们应该使通用的case快.最好把所有普通的优化技术应用到VM上.

关于VM还有很多要说:存储分配(allocation schemes),垃圾收集(garbage collection),保持他们稳定和高速,但是我想我将推延到下一部分.

下一次
bl_line.jpg (1610 bytes)

下一次我们将最终执行代码.然后我们就完成了我们的简单的脚本引擎.之后我可能给出一个复杂的真实的 脚本引擎的概貌,并且讨论所需的主题.

Quote!
gl_line.jpg (1418 bytes)

"Come," called the old man, "come now or you will be late."

"Late?" said Arthur. "What for?"

"What is your name, human?"

"Dent. Arthur Dent," said Arthur.

"Late, as in the late Dentarthurdent," said the old man, sternly. "It's a sort of threat you see." Another wistful look came into his tired old eyes. "I've never been very good at them myself, but I'm told they can be very effective."

HHG 1:22

Downloads
bl_line.jpg (1610 bytes)

Download the tutorial code (tut7.zip) (5k)

posted @ 2010-05-03 17:55 zffl 阅读(82) 评论(0) 编辑
摘要: 目录 [InAttribute] 和 [OutAttribute] 关键字 Out 和 Ref 以及通过引用传递 返回值 StringBuilder 和封送处理 复制和固定 内存所有权 反向 P/Invoke 和委托生存期 P/Invoke Interop Assistant 尝试一下 让我们面对现实吧。这个世界并不完美。几乎很少有公司在完全用托管代码开发程序,除此之外仍存在很多需要您处...阅读全文
posted @ 2010-03-07 00:58 zffl 阅读(259) 评论(0) 编辑

摘要:所有伟大的架构师都掌握了在抽象的不同层次上概念化解决方案的技能。通过将解决方案组织到离散的层次,架构师可以专注于解决方案的单个方面而忽略所有剩余的复杂性。展示将抽象层次应用到 IT 解决方案的技术,并将其与其他工程学科相比较。

本页内容

将抽象层次应用到 IT 解决方案 将抽象层次应用到 IT 解决方案 
抽象层次:所有工程师的强大武器 抽象层次:所有工程师的强大武器 
应用抽象层次时的核心原则 应用抽象层次时的核心原则 
将抽象层次应用到 IT 系统 将抽象层次应用到 IT 系统 
简单框架:四个抽象层次 简单框架:四个抽象层次 
通过迭代发展层次 通过迭代发展层次 
重访抽象层次核心原则 重访抽象层次核心原则 
扩展层次以支持企业解决方案 扩展层次以支持企业解决方案 
优点 优点 
小结 小结 
自我评估 自我评估 

将抽象层次应用到 IT 解决方案

企业架构师正受到其所面临的大量复杂性的挑战。开发一个能够自动处理企业任务的独立的部门应用程序是一回事。而设计并组成一个支持上万 IT 使用者的满是应用程序、服务器和数据库(全都支持多种企业活动)的 IT 实验室全球网络,则完全是另外一回事。要组合这些复杂性,IT 网络必须随时可用、响应迅速并保护企业宝贵的信息资产。除所有这些之外,IT 网络还必须足够灵活以支持企业永远变化的需要,并且采用出现的新技术。

一些架构师在这种复杂性方面明显非常出色,而且在不断进步。在我们的职业生涯中,能与一些真正伟大的分析师和架构师并肩工作是非常幸运的。反思这些经验,我们已经分析出是什么造就了杰出的架构师。

无一例外,所有伟大的架构师都掌握了在截然不同的抽象层次上概念化解决方案的技能。通过将解决方案组织到离散的层次,架构师可以将精力集中在解决方案的单个方面而忽略所有剩余的复杂性。他们一旦稳定了解决方案的某个部分,接下来就能继续处理其他方面,从而不断地将层次发展并完善到最终可以被实现的粘合模型中。

大多数软件开发人员懂得应该将解决方案分解到抽象层次。但是在实际的项目中,这是非常难于付诸实践的。当遇到第一个困难时,在急于开始编码时是很容易放弃这些层次的。伟大的架构师会经受这些挑战并在整个项目的生命周期中严格保持这些层次。他们意识到,如果不这样做,最终将淹没在复杂性中。

本文展示了将抽象层次应用到 IT 解决方案的技术。首先,我们会通过一个简单的示例演示此方法,然后提出一个基于正式抽象层次的系统产品的结构。

抽象层次:所有工程师的强大武器

其他的工程学科,比如土木工程师,几个世纪以来一直利用抽象层次复制复杂性。让我们学习一下其他更成熟的工程学科是如何应用抽象层次的,就从电子工程师开始吧,他们设计每次更新换代都变得更加复杂的计算机系统。

硬件工程师

系统设计师使用抽象层次为计算机系统建模。每个层次都是定义完善的,并提供了该系统的一个不同角度。许多系统是在三个主要层次上设计的:系统、子系统和组件,如 所示。

分层使工程师能够将庞大数量的复杂性集成到一个单一的工作计算机系统中。在其原子部分的层次上确切了解一台计算机是不可能的。在单独一块 Intel Itanium_ 芯片上有大约 25,000,000 个晶体管。

对 IT 相关学科来说,这种把复杂性分解到抽象层的方法当然不是惟一的。类似的方法被用于从航空工程到微生物学的无数其他学科。

应用抽象层次时的核心原则

所有工程师在应用抽象层次时都遵循这套核心原则。当把抽象层次应用到软件时,这些原则也同样适用。

这些层次的数量和范围是定义完善的,以便工程师能够在复杂的系统上协作,所有团队成员必须共享对层次的同一理解。只要设计师做出设计决定,他们必须将那些决定归档到相应的细节层次。

三个抽象层次定义如下:

greatarchitect_figithumb

 i. 定义的三个抽象层次

greatarchitect_figiithumb

 ii. 抽象层次的一个简单框架

每个层次内的多个视图

一个单个层次内的复杂性可以变得非常多,以至于使人无法一次全部掌握。在这种情况下,工程师通过多个视图将设计展现于单个层次内。每个视图展现设计的一个单独方面,但保持在相同的抽象层次上。举例来说,母板工程师为板的每个层创建一个视图,从而为每层的连接路径的设计建模。

greatarchitect_fig1

 1. 计算机系统的抽象层次

必须保持层次间的一致性

为了让系统按预期方式运行,每个后续的层必须是其父层的适当改进。如果计算机系统设计师从 IDE 总线切换到 SCSI 总线,那么所有设备的接口规范也必须切换到 SCSI。如果层次没有同步,那么系统就不会按预期方式在顶层执行。

将抽象层次应用到 IT 系统

既然我们已经分析了其他学科是如何应用抽象层次的,现在就让我们将此技术应用于 IT 解决方案1。下列部分展示了应用抽象层次为典型 IT 应用程序的需求、设计和实现建模的技术。这些技术是通过一个针对假想零售商的简单的、指导性的在线定单系统示例来展示的。在我们的示例中,我们不仅包括了体系结构,而且扩展了范围以包括系统需求和业务环境 — 如同由零售业所定义的。

简单框架:四个抽象层次

我们的简单示例定义 IT 解决方案的如下四个抽象层次:

  • 业务处理

  • 逻辑

  • 物理

在每个层次内,我们既展示了该特定层次行为的动态视图,又展示了其静态视图。动态视图为对象之间的消息建模,而静态视图为对象之间的结构和关系建模。

域抽象层次

应用了上面的范围规则,零售商就会作为域层次中的黑盒子中心的演员。客户作为外部的演员。域层次是从客户的角度来建模的。只为购买交互建模。用于完成购买的通讯形式不包括在这个层次,但是会在业务处理层次引入。

greatarchitect_fig2

 2. 关于从零售商处购买物品的域层次动态视图

greatarchitect_fig3

 3. 关于从零售商处购买物品的域层次静态视图
动态视图

域层次内的动态视图为客户和零售商之间的交互建模。下图汇总了域环境,并包含了简单的业务交互使用案例描述。

greatarchitect_fig4

 4. 关于从零售商处购买物品的业务处理层次动态视图
静态视图

域层次的静态视图为类结构和在使用案例中出现的它们的对象的关系建模。换句话说,它说明了在这个抽象层次上,为了完成购买交易客户需要了解什么对象。  展示了域层次静态视图的类关系图。

greatarchitect_fig5

 5. 关于从零售商处购买物品的业务处理层次静态视图

客户是 Person 的实例。客户和零售商之间的关系被具体化为 Account。所有的 Purchase 都与客户的 Account 相关。Purchase 与每个被购买的 Item 相关。每个 Item 都与特定的 Product 相关,这里 Product 遵循元类模式。Product 的实例实际上本身就是类。将其他 Product 添加到 Catalog 完全是一个数据驱动过程,而且不会对类模型产生影响,因此将 Product 建模为一个元类会使我们的模型更加灵活。围绕这些类,每个 Payment 都与其 Purchase 相关。

如您可能看到的,这个层次的模型对大多数零售商(无论类型为在线或传统,大型或小型)来说是有代表性的。这说明了为什么 [Industry] 域模型确实应该将公司定义为黑盒子中心的演员。同一个行业中的公司倾向于支持带有其外部演员的同一套业务交互。此外,域模型排除了公司的特定业务处理,这是因为在同一行业中的公司之间它们会有相当大的变化。

域层次严格集中在从外部演员的角度看到的业务交互。对此我们必须注意,不要将用于完成交互的实现机制包括进来。这些细节属于下一个抽象层次。因此,在本例中,我们只为浏览、选择、购买和支付建模。我们不为如何完成这些交互(通过电话、美国邮政、电子邮件、Web 应用程序、亲自前往、支票、信用卡或现金)建模。

业务处理抽象层次

下一个抽象层次为公司的业务处理建模,以实现在域层次捕获的交互。系统层次“内部缩放”公司的黑盒子,并标识为完成业务交易而协作的所有员工和系统。在这个层次,要开发的系统作为黑盒子中心的演员。

应用了系统层次的范围规则,在线定单系统就作为黑盒子中心的演员。客户和员工作为外部演员。系统层次是从客户和员工的角度来建模的。客户在线执行购买。支付是通过信用卡完成的。通过将物品运送到客户的收货地址履行定单。出货通知是由电子邮件发送的。

动态视图

动态视图重演了域层次购买交易,这次公开了零售商的内部业务处理。 汇总了业务处理环境,并包含了关于系统及其演员之间的交互的简单使用案例描述。

静态视图

这个层次的静态视图对类模型做了改进,以捕获在业务处理层次使用案例中出现的对象。换句话说,“为了在线创建一个定单并履行该定单,客户和雇员需要理解哪些对象?” 展示了业务处理层次静态视图的类关系图。我们修改域类模型以捕获在这个抽象层次上的角度。Person、Account 和 Company 抽象保持不变,Catalog 和 Product 也一样。但是,用 Order 替换了来自域模型的抽象 Purchase 事件。

Order 包括 LineItem,它与 Catalog 中的 Product 相关联。因为这个层次为公司的内部业务处理建模,所以我们需要获得现有的库存(最小库存单元 (SKU) 的一个属性,它表示在一个特定位置的物品的库存)。我们也为客户的 UserAccount 建模,它提供对在线系统的访问。Payment 是通过使用 CreditCardAccount 来完成的。Location 代表美国的一个地理位置,它作为账单邮寄地址,同时也作为 Order 的收货地址。Shipment 包含 Shipment 中包括的 Item。

我们在系统抽象层次创造方法来简化业务处理,因此该层次通常需要很多创造力。为此,通常使用业务处理层次上的若干不同形式来实现单个域层次交易。举例来说,一次购买可以通过在线、电话、邮件、传真一个定单表格或者亲自到零售店来完成。对于每一种形式,都需要在业务处理层次为其建模。请注意,尽管对零售商来说 Credit Authorizer 是一个外部演员,但是它还是在这个层次引入,这是因为只需要它实现在该层次首次出现的业务处理。

最后,请注意该系统是技术独立的。我们的在线购买系统可以用任何 Web 技术实现。在系统黑盒子内选择技术是一个体系结构决策。

逻辑抽象层次

逻辑层在系统黑盒子内缩放,从而公开高级别的系统设计。架构师选择技术并定义高级系统结构。在我们的简单示例中,系统是由承载表示层、业务层和数据访问层的 Microsoft IIS/Microsoft ASP.NET 服务器和承载持久性数据的 Microsoft SQL Server 数据库服务器组成的。

动态视图

逻辑层上的动态视图跟踪通过系统主要组件的消息流。如示例所示,在提交 ConfirmOrder Web 表单的时候, 跟踪这一消息流。

greatarchitect_fig6

 6. 从零售商处在线购买物品的逻辑层次动态视图
静态视图

这个层次的静态视图也将我们的视角切换到系统内部。尽管业务处理层次为出现在业务处理中的真实抽象建立了模型,这个层次将抽象建模为其在系统中所要被表示的那样。在实际的系统中,架构师会为每个软件层(表示层、业务层和数据访问层)设计类。为了保持本文的简洁, 7 只展示了业务层的静态设计,以便说明系统层抽象是如何针对设计进行改进的。

greatarchitect_fig7thumb

 7. 从零售商处在线购买物品的逻辑层次静态视图

架构师对系统层类进行改进以设计业务层接口。

因为系统中的所有账户和客户都是零售商的,所以创建一个单一的 Company 实例并使其与所有账户相关联是不切实际的,因此该层次中省略了 Company。我们只是存储 Payment 所带的信用卡号和账单邮寄地址,并非为每个 CreditCardAccount 创建一个单独的实例。此外,对系统来说,为每个出售的 Item 创建一个实例是不切实际的,因此从模型中删除了 Item,并改为由模型跟踪 LineItem 中订购的物品数量以及在新 ShippedItems 类中附带的物品数量。

架构师还定义业务层公开的服务间隔。对于本示例,业务层为 Account、UserAccount、Order、Shipment 和 Catalog 导出了 Create、Read、Update 和 Delete (CRUD) 服务。椭圆形指出了 CRUD 间隔。

请注意,即使本层次的类不是业务处理类的合适超集,架构师也可以通过直接改进业务处理类、将视角由系统外部更改为系统内部来实现这个设计。

物理抽象层次

物理抽象层次捕获系统实现的结构。系统作为一个节点的网络实现,每个节点都配置有硬件和软件。逻辑视图中的三个软件层(表示层、业务层和数据层)是以代码形式被物理实现,并部署到这些节点上。逻辑视图中的持久类物理存储在 SQL Server 数据库的关系表中。

动态视图

动态视图跟踪经过物理配置节点的消息流。ConfirmOrder HTTP post 从客户的浏览器通过 Internet 通过零售商的防火墙流动到 Web 服务器,在那里 Microsoft Windows 将其转发到 IIS,IIS 又将其传递到 Microsoft ASP.NET,然后 ASP.NET 调度 ConfirmOrder.aspx。幸运的是,现代开发工具将我们与多数物理网络隔离开来。但是,架构师需要了解物理层以避免网络瓶颈和安全暴露。

静态视图

静态视图( 8)将逻辑视图中的持久类改进为其物理表示形式。在我们的零售示例中,业务层类存储在下列 SQL Server 表中。

greatarchitect_fig8thumb

 8. 从零售商处在线购买物品的物理层次静态视图

映射到关系表和属性的类作为列实现。一对一关系和一对多关系使用一个外键来实现。开放式并发通过给每个被“凝结”的父类分配一个 datetime 字段来实现。

在设计逻辑层次时,架构师主要集中关注于实现系统功能。在确信包含了系统功能之后,架构师就能够专注于在物理层次优化实现。

通过迭代发展层次

建立了这个框架后,架构师通过几次迭代对解决方案加以发展。每次迭代都合并额外的功能 — 发票、待交定单、亲自订购、电话订购等等。在每种情况下,架构师都更新适当的抽象层次,然后将这些更新改进到物理实现层。

重访抽象层次核心原则

让我们对照核心抽象层次原则来测试我们的示例。

  • 这些层次的数量和范围是定义完善的:我们有四个不同的层次:公司黑盒子、系统黑盒子、系统内的逻辑设计以及物理实现。

  • 每个层次内的多个视图:在这个简单示例中,我们在每个层次上展示了一个动态视图和静态视图。

  • 必须保持层次间的一致性:如果对域模型作出了更改,则更改也一定会影响到较低层次。举例来说,如果零售商决定为其产品提供维护合同,分析师就会将MaintenanceContract 添加到域模型,并将其改进为其物理表现形式。对于维护大型系统来说,同步所有层次是很重要的。因为提交了增强请求,所以分析师执行对相应细节层次的影响评估。一些增强请求影响域层次(并且因此影响所有后续层次)。其他请求只影响物理层次。

扩展层次以支持企业解决方案

既然我们已经展示了带有四个抽象层次的简单示例,现在就让我们扩展这个方法来支持 IT 企业的解决方案。 9 展示了一个 Rational 统一过程 (Rational Unified Process,RUP) 配置,它将项目产品组织到定义完善的抽象层次中。

表中的层次描述如下。

greatarchitect_fig9thumb

 9. 将项目产品组织到定义完善的抽象层次中的 RUP 配置
  • 。域层次捕获项目的业务环境。

  • 项目洞察力。项目洞察力对系统将会有的对企业的业务影响进行通讯。它以投资回报分析量化了这个影响。项目洞察力表示该项目的最高抽象层次。

  • 业务处理。系统层次为公司内的业务处理建模。对于极其复杂的单位来说,这个层次可以再细分到子层次:部门、部门间以及部门内。

  • UI 规范。UI 规范设计了实现业务处理的用户界面。它是由 UI 设计文档和功能 UI 原型组成的。

  • 详细要求。详细要求指定了系统要求的最低层次抽象。它包括诸如数据类型格式和详细业务规则等详细信息。它还包括专业性要求,例如,性能、可用性、安全性、国际化、配置、可扩展性和灵活性要求等。

  • 体系结构。系统的体系结构被组织到六个视图中:

    • 逻辑。定义软件层和执行系统功能的主要抽象。

    • 并发。捕获系统的并行方面,包括交易、服务器场和资源争用。

    • 安全性。定义用于身份验证、授权、保护机密和日志记录的方法。

    • 部署。定义网络拓扑和系统的部署配置。

    • 组件。定义系统组件、其接口以及依赖项。

    • 数据。定义持久性数据的设计结构。

优点

将系统产品组织到离散的抽象层次有若干优点:

  • 它将系统要求分离到三个不同的抽象层次:业务处理、UI 规范和详细要求。我们不会再用令企业用户感到不知所措的单个整体功能规范了。取而代之,我们在三个改进的详细层次中对系统要求进行通讯。

  • 分析师和架构师可以将复杂性控制在一个单一的、集成的系统模型中。

  • 架构师可以专注于系统的单个方面,并将那些决策集成到整个解决方案中。

  • 抽象层次形成了系统产品的结构。举例来说,软件体系结构文档为每个视图专设了一个小节。

  • 抽象层次提供从要求到设计再到实现的直接可跟踪能力。可跟踪能力使小组能够在评测更改请求时执行精确的影响评估。

  • 在使用同一框架开发几个系统之后,会在每个抽象层次形成模式。单位可以编录这些模式和每个抽象层次内的其他最佳实践。这个最佳实践的目录会作为过程改进计划的基础。

小结

为了处理复杂性,所有工程学科都应用正式抽象层次。软件也不例外。为了实现抽象层次的优点,项目必须:

  • 正式标识层次,每个层次都有定义完善的范围。

  • 将一个层次内的复杂性分开到多个视图。

  • 在层次间保持一致性。

通过一个简单的示例,本文演示了如何应用抽象层次,然后将该方法扩展到支持企业 IT 解决方案。它提供了一个 RUP 配置框架,该框架将系统产品组织到定义完善的抽象层次。

自我评估

  • 您当前的项目是否应用了抽象层次?

  • 层次是否定义完善?

  • 项目团队是否很好地理解了这些层次?

  • 如果复杂性在一个层次中变得过大,团队是否将其分离到视图中呢?

  • 团队是否在层次间保持一致性?

  • 您的项目会从抽象层次中获益吗?

伟大的架构师本能地遵循这些原则。我们其余的人就必须有意识地应用抽象层次,并运用规则在整个项目生命周期中保持这些层次。

资源

Cockburn, Alistair. Writing Effective Use Cases. New Jersey: Addison-Wesley, 2001

Kroll, Per and Kruchten, Philippe. The Rational Unified Process Made Easy: A Practitioner's Guide to the RUP. Boston MA: Pearson Education and Addison-Wesley, 2003

DeMarco, Tom and Plauger, P J Structured Analysis and System Specification. Prentice Hall PTR, 1979

要获得 DoD 标准 2167A 的联机副本,请访问 http://www2.umassd.edu/SWPI/DOD/MIL-STD-2167A/DOD2167A.html

脚注

1 很多人已经成功地将抽象层次应用于软件。Ed Yourdon 和 Tom DeMarco 在 1979 年提出了结构化分析和结构化系统设计的概念。美国政府的许多分支机构标准化了 DoD 的 2167A 标准,它要求系统由有层次的硬件和软件配置项组成。DBA 社区经常应用细节层次为关系数据库建模。特别地,Bachman 工具集和 James Martin 的信息工程方法学 (Information Engineering Methodology,IEM) 先为数据库逻辑建模,然后再为其物理建模。在 Google 上键入“software levels of abstraction”进行搜索会返回若干个结果,但其中大多数来自于学术社区,而且其内容看起来集中在正式计算机语言方面。

关于作者

Don Awalt 是 RDA Corporation 的创始人和 CEO,该公司是一家自定义软件工程公司,成立于 1988 年,在华盛顿特区、巴尔的摩、亚特兰大、费城和芝加哥都设有办事处。作为微软认证金牌伙伴 (Microsoft Gold Certified Partner),该公司专注于使用 .NET Framework 开发企业 Web 和富客户端系统。Don 目前是 Microsoft 华盛顿特区的区域总监,他以前曾经担任费城首任区域总监。Don 经常在行业活动中演讲,这些活动包括 Tech Ed、Developer Days、MSDN 活动和各种 SQL Server 及 Windows 活动。他是 SQL Server Magazine 和 PC Tech Journal Magazine 的特约编辑,并且也为其它出版物撰写稿件。Don 所擅长的技术领域包括 Web 服务、SQL Server、现代编程语言的发展,以及在 Microsoft 的 Prescriptive Architecture Group (PAG) 中可以看到的许多体系结构和处理工作。可以通过 AWALT@rdacorp.com 联系到 Don。

Rick McUmber 是 RDA 的质量和最佳实践总监。他为 IBM 和 Rational Software Corporation 一共工作了 11 年,致力于为美国运输部、国防部、NASA 和加拿大国防部开发系统。从 1994 年以来,他一直在 RDA 工作,致力于为其客户开发业务解决方案。可以通过McUmber@rdacorp.com 联系到 Rick。

转到原英文页面

posted @ 2010-02-08 13:29 zffl 阅读(56) 评论(0) 编辑

 转自:http://www.microsoft.com/china/MSDN/library/netFramework/netframework/NETMattersSep.mspx?mfr=true

问 我一直在使用 .NET Framework 2.0 中的新 TransactionScope 类,我喜欢它所提供的模型。要启动一个事务,我可以在一个方法中使用 Transaction 创建一个 TransactionScope,然后在该方法所调用的另一方法中,可以创建一个自动列在该 Transaction 中的 SqlCommand。但 SqlCommand 如何知道先前创建的 Transaction?更重要的是,如何在我自己的类中模拟此功能?

答 如果您还不太熟悉 TransactionScope,那么告诉您,TransactionScope 是 Microsoft® .NET Framework 2.0 中新增的 System.Transactions 命名空间的一部分。System.Transactions 提供了完全集成到 .NET Framework 中的事务框架(包括但不局限于 ADO.NET)。Transaction 和 TransactionScope 是此命名空间中最重要的两个类。正如问题中所说,您可以创建一个 TransactionScope 实例,然后会在其中自动列出在该 TransactionScope 的作用域内执行的 ADO.NET 操作(您也可以通过 Transaction.Current 静态属性访问当前的 Transaction):

using(TransactionScope scope = new TransactionScope())
{
    ... // 此处的所有操作均为事务的一部分
    scope.Complete();}

TransactionScope 有多个构造函数,其中一些接受 TransactionScopeOption 枚举值,告诉 TransactionScope 是否创建新事务、是否使用任何可能已经存在的环境事务或者是否取消任何环境事务。如果我执行以下代码(这段代码将创建一个嵌套 TransactionScope,它需要新事务而不使用现有环境事务),我将看到首先输出原始 Transaction 的局部标识符,接着是新 Transaction 的标识符,然后又是原始标识符:

using (new TransactionScope())
{
    Console.WriteLine(
        Transaction.Current.TransactionInformation.LocalIdentifier);
    using (new TransactionScope(TransactionScopeOption.RequiresNew))
    {
        Console.WriteLine(
            Transaction.Current.TransactionInformation.LocalIdentifier);
    }
    Console.WriteLine(
        Transaction.Current.TransactionInformation.LocalIdentifier); 
}

可以嵌套任意数量的 TransactionScope,在内部,System.Transaction 命名空间保留所有环境事务的堆栈。

在其核心,线程静态成员提供了以下模型。如果一个类型包含非静态字段(实例字段),则对于该字段,该类型的每个实例均有其自身的独立存储位置;在一个实例中设置字段并不影响其他实例中该字段的值。而相反,对于静态字段,无论有多少实例,该字段只位于一个存储位置(或者,更具体地说,在每个 AppDomain 中,只位于一个存储位置)。然而,如果将 System.ThreadStaticAttribute 应用于静态字段,则该字段将变为线程静态字段,即,对于该字段,每个线程(而非实例)将保留其自身的存储位置。在一个线程上设置线程静态的值将不会影响其在其他线程上的值。熟悉 Visual C++® 的人会知道,在概念上,此功能与 __declspec(thread) 中的该功能相似,用于在线程局部存储中声明线程局部变量。

线程静态字段可用于各种情况。一种常见用法是用于存储非线程安全类型的单例。通过为每个线程而不是整个 AppDomain 创建单例,并不需要进行显式锁定就可确保线程安全,因为只有一个线程可以访问变量,也就只有一个线程可以访问实例(当然,如果线程将对单例对象的引用交给另一线程,这种安全性将会丧失)。实际上,当使用静态单例时,通常需要两种锁定,一种用于初始化(如果初始化在类型的静态构造函数中完成,则属于隐式锁定,如果使用延迟初始化,则属于显示锁定),另一种用于访问实际实例。线程静态单例不需要这两种锁定。

线程静态字段的另外一种用法是(这又将我们带回到 TransactionScope)用于在方法调用之间传递带外数据。在非面向对象的语言中,通常情况下,函数有权访问的数据只能是通过参数或全局变量显示提供给它的数据。但是,在面向对象的系统中,还有许多其他方式,如,实例字段(如果方法为实例方法)或静态字段(如果方法为静态方法)。

您可以假设 TransactionScope 可以使用静态字段来存储当前的 Transaction,而 SqlCommand 只从该静态字段中获取相应 Transaction。然而,在多线程情况下,这可能会引发重大问题,因为两个 TransactionScope 可能会相互攻击。某一线程可能会覆盖另一线程最近发布的当前 Transaction 引用,这样,多个线程最终可能会使用同一 Transaction 实例,所有这些可能会引发一场灾难。

这回,线程静态字段有了用武之地。既然存储在线程静态字段中的数据只对存储该数据的同一线程中所运行的代码可见,那么,可使用此类字段将其他数据从一个方法传递到该第一个方法所调用的其他方法,而且完全不用担心其他线程会破坏它的工作。现在,假设 TransactionScope 使用了相似的技术。实例化后,它会将当前的 Transaction 存储到线程静态字段中。当稍后实例化 SqlCommand 时(在此 TransactionScope 从线程局部存储中删除之前),该 SqlCommand 会检查线程静态字段以查找现有 Transaction,如果存在则列入该 Transaction 中。通过这种方式,TransactionScope 和 SqlCommand 能够协同工作,从而开发人员不必将 Transaction 显示传递给 SqlCommand 对象。实际上,TransactionScope 和 SqlCommand 所使用的机制非常复杂,但核心是前提要合理。

可以使用线程静态字段创建任何类型的相似系统。图 1 显示了我编写的一个类:Scope<T>,它实现了有些相似的行为。理念是:您可以在一个方法中使用类型 T 的一个实例来实例化 Scope<T>,并希望调用堆栈中的另一方法稍后能够访问该实例。该方法可以使用 Scope<T>.Current 来访问实例。Scope<T> 也支持嵌套,即在内部,它保留类型 T 的实例堆栈,并通过 Current 属性将实例暴露在堆栈顶部。这样,如果一个方法创建了具有一个 T 类型实例的 Scope<T>,然后调用了用于实例化具有其他 T 类型实例的另一 Scope<T> 的其他方法,则第一个 Scope<T> 中的实例不会丢失,并可在删除嵌套作用域后,通过 Scope<T>.Current 再次进行访问。

应几乎始终严格限制环境属性(如 Scope<T>.Current)的作用域。换句话说,需在同一堆栈帧中发布和撤销这些属性(通常是通过 try/finally 块)。对于 TransactionScope、锁定、模拟等等也是如此。这也是 Scope<T> 实现 IDisposable 的原因:Scope<T> 将用于 using 块中,以便构造函数发布实例以及 Dispose 方法撤销该实例。

下面的示例应该可以帮助您巩固上面介绍的内容。图 2 显示了如何使用具有 StreamWriter 实例的 Scope<T> 的一个示例。首先,Main 方法将“MainEnter”写到由 Scope<StreamWriter>.Current 返回的 StreamWriter 中。因为当前没有处于活动状态的 Scope<StreamWriter>,Scope<StreamWriter>.Current 将返回空(这就是我使用了一个助手 Write 方法的原因,该方法用于在向 StreamWriter 中写入之前检查其是否为空,否则必定会生成 NullReferenceException)。然后,针对文件 C:\test1.txt,使用 StreamWriter 实例化新的 Scope<StreamWriter>,并调用 FirstMethod 方法。FirstMethod 将“FirstMethodEnter”写到当前 StreamWriter 中,同时,该文本将输出到 C:\test1.txt 中,因为它是当前作用域中的 StreamWriter。FirstMethod 继续设置一个新的 Scope<StreamWriter>,这次是针对 C:\test2.txt。在内部,对于 Scope<T>,这将导致新 StreamWriter 被压入内部堆栈,位于 C:\test1.txt 的现有 StreamWriter 之上。此时,Scope<StreamWriter>.Current 将返回对 C:\test2.txt 的 StreamWriter 的引用,因此,对 SecondMethod 的调用将导致“SecondMethod”被写入 C:\test2.txt,而不是 C:\test1.txt。FirstMethod 继续删除 C:\test2.txt 的 Scope<StreamWriter>(借助于 using 关键字),将该 StreamWriter 从内部堆栈中弹出,使 C:\test1.txt 的 StreamWriter 恢复为当前 StreamWriter,这样,在将“FirstMethodExit”写入 Scope<StreamWriter>.Current 时会将该文本指向 C:\test1.txt。最后,返回到 Main 中,原始 Scope<StreamWriter> 被删除,StreamWriter 的内部堆栈清空,这样,Scope<StreamWriter>.Current 再次返回空。好了,这下放心了。

在内部,Scope<T> 依赖线程静态字段存储提供给 Scope<T> 构造函数的实例的 Stack<T>。每次构造 Scope<T> 时,会从 Instances 属性中检索线程的 Stack<T>,并将新 T 压入其中。静态 Current 属性使用 Stack<T>.Peek 方法返回位于堆栈顶端的 T 实例(如果存在),如果 Stack<T> 为空则返回空。当删除 Scope<T> 时,顶端项将从堆栈弹出。

如果您再回头看看图 1,您可能想知道,对于 Stack<T> 字段 _instances,我为什么使用了延迟初始化。要知道,必须创建该实例,Scope<T> 构造函数才能成功运行,因此延迟初始化矫枉过正了,不是吗?当然不是,我本可以删除延迟初始化,然后只需更改该字段的声明将其初始化包括在内:

private static Stack<T> Instances { return _instances; }
[ThreadStatic]
private static Stack<T> _instances = new Stack<T>();

然而,如果进行上述更改并在多个线程中使用了 Scope<T>,则当我试图构造 Scope<T> 的新实例时,我可能会开始看到抛出的 NullReferenceException。尽管该字段已用 ThreadStatic 属性进行标记,但该字段的初始化实际上是类型的静态构造函数的一部分,因此它只执行一次并只在运行类型的静态构造函数的线程(第一个赢得其状态访问权限的线程)上执行。该线程上使用的所有 Scope<T> 均可正常工作,因为 _instances 字段已经进行了正确的初始化,但是在所有其他线程上,_instances 将仍为空,从而导致在每次 Scope<T> 的构造函数试图将某项压入不存在的堆栈时均会引发 NullReferenceException。

以下是您应该注意的其他几项实现细节。第一,将实例从堆栈弹出后,Dispose 方法将检查堆栈中是否还存在任何剩余的实例。如果不存在,它会将线程静态 Stack<T> 字段设置为空。线程静态字段具有一个很好的属性,即一旦托管线程消失,就不会再将它们视为 GC 根。不过,如果使用 Scope<T> 的线程偶尔具有很长的生存期,则所包含的 Stack<T> 被保留的时间可能会大大超过所需时间。如果随时间推移,有大量线程使用 Scope<T>,则可能导致膨胀。因此,我建议仅当堆栈中包含实例时才向其中压入实例,当堆栈为空时立即将堆栈释放。

其他需要注意的事项是对 Thread 类的静态 BeginThreadAffinity 和 EndThreadAffinity 方法的调用。之所以存在这两个方法是因为 .NET Framework 2.0 最初允许 CLR 宿主提供其自身的线程管理,允许宿主将托管线程作为纤程运行以及提供其自身的纤程调度。这样,CLR 宿主可随时将正在执行的任务从一个物理 OS 线程移动到另外一个。但根据托管代码所处理的具体内容的不同,正在执行的任务可能具有必需的线程关联度,这意味着在任务执行期间,它们可能需要位于同一物理 OS 线程上。因此,引入了 BeginThreadAffinity 和 EndThreadAffinity 方法,以便在请求线程关联度时,允许托管代码通知宿主不能移动任务。由于 Scope<T> 使用的是线程局部存储(其与物理线程而非纤程相关联),因此使用这两个方法是很重要的。否则,当执行一个线程时,Scope<T> 可以将数据存储到线程静态中,但在不为 Scope<T> 所知的情况下,任务就可能被移动到具有完全不同线程静态值的另一 OS 线程中。实际上,已经不赞成对纤程的支持,因此当前没有正确调用这两个方法不会产生任何危害。也就是说,在 Framework 将来的版本中很有可能会重新引入对纤程的支持,因此您现在应该养成使用这两个方法的习惯,以避免以后在调试时非常头疼。

最后,请注意 Scope<T> 不但将提供的实例压入了 Stack<T>,还将其存储在实例字段中。这完全是用来验证 Scope<T> 的作用域是否正确及其清理的顺序是否正确。将实例从堆栈弹出后,该实例会与 Scope<T> 中缓存的实例进行比较;这两个实例应该始终相同。如果不相同,则删除嵌套 Scope<T> 实例的顺序错误。

当然,既然我们已将用户提供的实例存储在 Scope<T> 中,那么是否还确实需要 Stack<T>?实际上,我们可以将其取消,只需将嵌套的 Scope<T> 实例链接到一起成为一个链接列表,将列表头存储在线程静态中,每个 Scope<T> 均存储对前一 Scope<T> 的引用。这不仅简化了设计,而且还删除了一些与维护 Stack<T> 相关的当前不必要分配。图 3 显示了更新的 Scope<T> 版本。

问 我喜欢 .NET Framework 中的新 Semaphore 类,但是它没有 Monitor 类(其中 C# 和 Visual Basic® 提供了 lock 和 SyncLock 关键字)易用。为什么 Semaphore 没有提供相似的功能呢?

答 通过一小段代码,它就会变得同样简单了。图 4 包含了一个轻量级包装类,您可以使用它模拟 lock/SyncLock 行为,只是用 Semaphore 替代了 Monitor。Disposable.Lock 是接受 Semaphore 的静态方法,等待 Semaphore,然后返回用于实现 IDisposable 的类的新实例;以此实例调用 Dispose 来释放 Semaphore。以下这段代码允许您对 Semaphore 使用 using 关键字,以便等待它,执行一些工作,然后将其释放:

using(Disposable.Lock(theSemaphore))
{
    ... // 在此做工作
}

图 4 中的实现很简单,但是它使用了两个您可能不熟悉的方法:Thread.BeginCriticalRegion 和 Thread.EndCriticalRegion。临界区是指在此区域内异步或未处理异常的影响可能不仅局域于当前任务,而且还可能造成整个 AppDomain 不稳定。例如,在处理一些跨线程数据时,某线程可能被中断,并且可能没有机会将一切重置为有效状态。如果在临界区中出现故障(如 ThreadAbortException),CLR 宿主(如 SQL Server™ 2005)可以选择停止整个 AppDomain,而并不冒险在可能不稳定的状态下继续执行。当线程进入或退出临界区时需要通知 CLR,以便在出现故障情况时,CLR 能够相应地通知宿主。这就是 Thread.BeginCriticalRegion 和 Thread.EndCriticalRegion 存在的原因。既然锁定用于线程间通信,那么取消锁定会开启临界区,而释放锁定会结束临界区,这是行得通的。此逻辑内置于 Monitor 中,因此在图 4 中我已将其包括在 Disposable.Lock 实现之中。有关详细信息,请参阅 MSDN® 杂志 2005 年 10 月刊中我的一篇文章,网址为 msdn.microsoft.com/msdnmag/issues/05/10/Reliability

请将您的疑问和意见通过netqa@microsoft.com 发送给 Stephen。

Stephen Toub 是 MSDN 杂志的技术编辑。

本文摘自 2006 年 9 月发行的 MSDN 杂志

转到原英文页面

posted @ 2010-02-08 13:23 zffl 阅读(37) 评论(0) 编辑
摘要: 本文讨论:紧密耦合体系结构的错误之处测试和依赖关系灾难依赖关系反转依赖关系注入本文使用了以下技术:.NET Framework代码下载位置:DependencyInjection2008_03.exe(5408 KB)Browse the Code Online目录内部依赖关系问题依赖关系反转依赖关系注入容器成熟的 IoC 容器生存期管理自动绑定依赖关系轻松实现更改几乎所有人都认为追求松散耦合设计...阅读全文
posted @ 2010-02-03 16:49 zffl 阅读(48) 评论(0) 编辑
摘要: 随着电子商务的发展,越来越多的购物网站出现啦。由于电子购物网站的进入门槛低,因此在互联网上出现了纵多良莠不齐的购物网站。湖南本地购物网站做为一个地方性购物网站,其发展存在很大的制约,同时也存在一些优势。下面主要从湖南本地购物网站发展的劣势与发展方向来分析。湖南本地购物网站面临的制约:市场份额作为湖南电子商务发展的主力军,湖南本地购物网站发展都比较晚,在一些大型的电子商务购物网站成立后,采取模仿其模...阅读全文
posted @ 2009-10-08 13:42 zffl 阅读(359) 评论(0) 编辑
摘要: http://msdn.microsoft.com/zh-cn/library/ms978723.aspx上下文您已经决定使用Model-View-Controller (MVC) 模式将动态 Web 应用程序的用户界面逻辑与业务逻辑分隔开来。您已经考察了Page Controller模式,但您的页面控制器类具有复杂的逻辑,并且是较深的继承层次结构的一部分,或者,您的应用程序是基于可配置的规则来动...阅读全文
posted @ 2009-09-29 14:06 zffl 阅读(60) 评论(0) 编辑