最近一段时间由于工作需要,仔细研读了微软企业库的部分源码,不由得佩服这些大洋彼岸的同行们.先不谈代码的架构怎么样,起码在代码注释这一块,那叫一个专业啊.一个200行的源文件150行注释50行代码是常有的事.注释量不仅多,质量也高.我的很多困惑都是通过阅读代码注释得以解答的.
这年头,代码注释的方式基本都是采用以///开头的xml注释方式了.在visual studio里,连续输入三个///,编辑器会自动补全剩下的部分.默认使用的是summary标签.如果是方法则可能还会有param与returns标签.这也是我们最常用到的三个标签.难道xml注释方式只有这三种标签吗?显然不是.当你再输入一个<的时候,编辑器就会自动提示还可以使用的标签.粗粗一算,整整20个,可真不少呀.
上网查阅了一下,其实常用的只有11个标签左右,下面大概介绍一下
summary,被注释对象的摘要
example,展现被注释对象的一个示例
c,在注释中写代码,当代码只有一行的时候使用.通常与example配合使用
code,在注释中写代码,当代码有多行时使用.通常与example配合使用
param,被注释对像的参数,有一个name属性,指定注释哪一个参数
returns,被注释对像的返回值
exception,被注释对象执行时可能抛出的异常.有一个cref属性,指定异常类型
remarks,被注释对象的备注.用来详细描述被注释对象
para,段落标签. 类似于HTML里的P标签.写在里面的内容会作为单独的一段
paramref,引用参数.在摘要,备注或其它地方如果要对参数进行说明,如果将参数以该标签包裹,最终生成的chm文档时会生成一个超键接指向该参数,有一个name属性,指定参数名称
see,参考标签.有一个cref属性,指定参考类型名.
以上11个标签在使用上各不相同.首先,只有被summary包裹或param,returns,para这四个标签才能在visual studio的智能感知里看到效果.你单独写一个remarks标签vs是感知不到的,其它的注释效果需要用工具生成chm后才能看到.其次,summary,example,returns,remarks,para会成普通文字格式,c,code会生成代码格式,exception,see会生成一个超链接链向指定类型.更加详细的说明可参考本文最后的链接.
现在再来谈一谈注释生成工具的使用.最早的工具是微软的御用工具DocGen,开源的NDoc,还有一个通用工具DoxyGen,但是现在这几个要么不好用,生成的文档不符合.net习惯,要么软件停止更新了,不好获取了.现在用的比较多的是Sandcastle.个人体验了一下,效果不错!更加详细的说明可参考本文最后的链接.
文章的最后还是放上一段个人比较喜爱的一句话,与君共勉:
傻瓜都可以写出机器能读懂的代码,但只有专业程序员才能写出人能读懂的代码
参考的文章:
- C# and XML Source Code Documentation
事情的初衷很简单,就是想不用xml配置来使用其缓存组件,试了很多遍都无法成功.不得已安装了其源码大略分析一遍,才总算成功.后来又一想,既然分析就分析的彻底一点吧,顺便看看国外的高手们是怎么架构组件,书写代码的,于是就有了这篇文章.企业库为5.0版本.
首先是类关系图:

缓存组件的整体结构为CacheManager -> Cache -> CacheItem,其中CacheItem为缓存项,其有Key有Value,还有本缓存项的过期策略及删除时的回调函数.Cache为缓存,除管理CacheItem外,还负责管理缓存性能计算器及缓存持久化.CacheManager为Cache类的包装类,用户调用接口,也是最为我们熟悉的,其代理了Cache类的缓存操作方法,此外还有过期轮询等.下面就来一步一步的分析.
一.缓存创建

常见的缓存创建方式为:
ICacheManager manager = Microsoft.Practices.EnterpriseLibrary.Caching.CacheFactory.GetCacheManager();
其实还有一种创建方式:
CacheManagerFactory factory = new CacheManagerFactory();
ICacheManager manager = factory.CreateDefault();
这两种方式创建缓存,本质上调用的都是这段代码:
EnterpriseLibraryContainer.Current.GetInstance<ICacheManager>(cacheManagerName)
EnterpriseLibraryContainer对象,又称企业库容器对象,说白了就是个依赖注入的容器,封装了unity框架.更具体的说明,请参见我另写的一篇文章:代码级浅析企业库对象创建
这段代码的意思,就是返回一个注册了的实现了ICacheManager接口的类.这里实际返回的是CacheManager类.
二.缓存轮询

缓存配置中有两个参数用在了这里:numberToRemoveWhenScavenging和maximumElementsInCacheBeforeScavenging.参数的名字已经把他们的用途说的很明白了:缓存里存储了多少项数据后启动清理及每次移除多少项数据.
1 public void Add(string key, object value, CacheItemPriority scavengingPriority, ICacheItemRefreshAction refreshAction, params ICacheItemExpiration[] expirations)
2 {
3 realCache.Add(key, value, scavengingPriority, refreshAction, expirations);
4
5 backgroundScheduler.StartScavengingIfNeeded();
6 }
代码第六行说的很清楚,每次新增缓存项时,就会检查缓存项是否超过了配置值.如果超过了,就会通过多线程的方式在线程池中执行以下方法
internal void Scavenge()
{
int pendingScavengings = Interlocked.Exchange(ref scavengePending, 0);
int timesToScavenge = ((pendingScavengings - 1) / scavengerTask.NumberOfItemsToBeScavenged) + 1;
while (timesToScavenge > 0)
{
scavengerTask.DoScavenging();
--timesToScavenge;
}
}
然后又调用了ScavengerTask类的DoScavenging方法
1 public void DoScavenging()
2 {
3 if (NumberOfItemsToBeScavenged == 0) return;
4
5 if (IsScavengingNeeded())
6 {
7 Hashtable liveCacheRepresentation = cacheOperations.CurrentCacheState;
8
9 ResetScavengingFlagInCacheItems(liveCacheRepresentation);
10 SortedList scavengableItems = SortItemsForScavenging(liveCacheRepresentation);
11 RemoveScavengableItems(scavengableItems);
12 }
13 }
这是实际实现功能的方法.如果缓存项多于配置值时就会执行.第9行代码将缓存项的eligibleForScavenging字段设为true,表示可以对其做扫描移除工作.其实与这个字段相对应的EligibleForScavenging属性并不是简单的返回这个字段,其还考虑了缓存项的优先级,只有eligibleForScavenging为true且优先级不为最高(NotRemovable),才返回true.第10行即对缓存项做排序工作,以优先级为排序字段将缓存排序,优先级最高的排在后面,表示最后才被删除.第11行则是真正删除方法.在其方法体内会遍例排序之后的缓存项,如果EligibleForScavenging属性为true则删除,还有个变量记录了删除的个数.如果其等于配置值,则停止删除.
可以看到缓存轮询与缓存过期无关,缓存优先级与缓存过期也没关系.那么经过扫描后的缓存,仍然可能存在已过期项.
三.缓存过期

在配置文件中有一个配置与此有关:expirationPollFrequencyInSeconds,则每隔多长时间对缓存项进行一次过期检查.
pollTimer.StartPolling(backgroundScheduler.ExpirationTimeoutExpired);
在缓存容器CacheManger创建时就会开始计时
pollTimer = new Timer(callbackMethod, null, expirationPollFrequencyInMilliSeconds, expirationPollFrequencyInMilliSeconds);
其本质是一个Timer对象,定时回调指定的函数.这里的回调函数其实是BackgroundScheduler对象的Expire方法:
internal void Expire()
{
expirationTask.DoExpirations();
}
其又调用了ExpirationTask对象的DoExpirations方法:
1 public void DoExpirations()
2 {
3 Hashtable liveCacheRepresentation = cacheOperations.CurrentCacheState;
4 MarkAsExpired(liveCacheRepresentation);
5 PrepareForSweep();
6 int expiredItemsCount = SweepExpiredItemsFromCache(liveCacheRepresentation);
7
8 if(expiredItemsCount > 0) instrumentationProvider.FireCacheExpired(expiredItemsCount);
9 }
这里是过期的实际功能方法.代码第四行遍例缓存,将已过期的缓存的WillBeExpired属性标记为true,第6行则是将所有WillBeExpired属性标记为true的缓存项进行删除.下面来看如何判断一个缓存项是否过期.
其实新增缓存的方法有多个重载,其中一个就是
public void Add(string key, object value, CacheItemPriority scavengingPriority, ICacheItemRefreshAction refreshAction, params ICacheItemExpiration[] expirations)
常见的有绝对时间,相对时间,文件依赖等.可以看到,一个缓存项,是可以有多个缓存依赖的,或者叫缓存过期策略.如果其中任意一个过期,则缓存项过期.
public bool HasExpired()
{
foreach (ICacheItemExpiration expiration in expirations)
{
if (expiration.HasExpired())
{
return true;
}
}
return false;
}
对缓存项过期的管理,除定时轮询外,在取值的时候,也会判断.
四.缓存回调
如果缓存项从缓存中移除,则会触发回调:
RefreshActionInvoker.InvokeRefreshAction(cacheItemBeforeLock, removalReason, instrumentationProvider);
实际上是以多线程的方式在线程池中执行回调函数
public void InvokeOnThreadPoolThread()
{
ThreadPool.QueueUserWorkItem(new WaitCallback(ThreadPoolRefreshActionInvoker));
}
private void ThreadPoolRefreshActionInvoker(object notUsed)
{
try
{
RefreshAction.Refresh(KeyToRefresh, RemovedData, RemovalReason);
}
catch (Exception e)
{
InstrumentationProvider.FireCacheCallbackFailed(KeyToRefresh, e);
}
}
五.线程安全
企业库用了大量的代码来实现了缓存增,删,取值的线程安全.它用了两个锁来实现线程安全.新增操作最复杂,就分析它吧
1 public void Add(string key, object value, CacheItemPriority scavengingPriority, ICacheItemRefreshAction refreshAction, params ICacheItemExpiration[] expirations)
2 {
3 ValidateKey(key);
4
5 CacheItem cacheItemBeforeLock = null;
6 bool lockWasSuccessful = false;
7
8 do
9 {
10 lock (inMemoryCache.SyncRoot)
11 {
12 if (inMemoryCache.Contains(key) == false)
13 {
14 cacheItemBeforeLock = new CacheItem(key, addInProgressFlag, CacheItemPriority.NotRemovable, null);
15 inMemoryCache[key] = cacheItemBeforeLock;
16 }
17 else
18 {
19 cacheItemBeforeLock = (CacheItem)inMemoryCache[key];
20 }
21
22 lockWasSuccessful = Monitor.TryEnter(cacheItemBeforeLock);
23 }
24
25 if (lockWasSuccessful == false)
26 {
27 Thread.Sleep(0);
28 }
29 } while (lockWasSuccessful == false);
30
31 try
32 {
33 cacheItemBeforeLock.TouchedByUserAction(true);
34
35 CacheItem newCacheItem = new CacheItem(key, value, scavengingPriority, refreshAction, expirations);
36 try
37 {
38 backingStore.Add(newCacheItem);
39 cacheItemBeforeLock.Replace(value, refreshAction, scavengingPriority, expirations);
40 inMemoryCache[key] = cacheItemBeforeLock;
41 }
42 catch
43 {
44 backingStore.Remove(key);
45 inMemoryCache.Remove(key);
46 throw;
47 }
48 instrumentationProvider.FireCacheUpdated(1, inMemoryCache.Count);
49 }
50 finally
51 {
52 Monitor.Exit(cacheItemBeforeLock);
53 }
54
55 }
代码第10行首先锁住整个缓存,然后新增一个缓存项并把他加入缓存,然后在第22行尝试锁住缓存项并释放缓存锁.如果没有成功锁上缓存项,则重复以上动作.在代码14与15行可以看到,这时加入缓存的缓存项并没有存储实际的值.他相当于一个占位符,表示这个位置即将有值.如果成功锁住了缓存项,代码第39号则是以覆盖的方式将真正的值写入缓存.
这里为什么要用两个锁呢?我觉得这是考虑到性能.用一个锁锁住整个缓存完成整个操作固然没有问题,但是如果代码第33行或第38号耗时过多的话,会影响整个系统的性能,特别是第38行,涉及IO操作,更是要避免!那为什么在第14行使用的是占位符而不是真正的存储呢?我觉得这也是考虑到性能.这里的新增操作包括两个含义,缓存中不存在则新增,存在则更新.这里考虑的是更新的问题.通常做法是让缓存项指向新对象,这样先前指向的对象就会成为垃圾对象.在高负载的应用程序里,这会产生大量的垃圾对象,影响了系统的性能.如果通过Replace的方式来操作,则可以必免这个问题,让缓存项始终指向一个内存地址,只是更新他的内容而以.
六.离线存储(缓存持久化)

通过这个功能,可以让内存数据保存在硬盘上.在缓存初始化的时候会从硬盘上加载数据
Hashtable initialItems = backingStore.Load();
inMemoryCache = Hashtable.Synchronized(initialItems);
在新增与删除的时候,会在硬盘上做相应的操作
backingStore.Add(newCacheItem);
backingStore.Remove(key);
在企业库里,是通过.net的IsolatedStorageFile类来实现其功能的.每个缓存都对应一个目录
private void Initialize()
{
store = IsolatedStorageFile.GetUserStoreForDomain();
if (store.GetDirectoryNames(storageAreaName).Length == 0)
{
// avoid creating if already exists - work around for partial trust
store.CreateDirectory(storageAreaName);
}
}
每个缓存项则是一个子目录,缓存项里的每个对象则被序列化成单个文件
return Path.Combine(storageAreaName, itemToLocate);
1 public IsolatedStorageCacheItem(IsolatedStorageFile storage, string itemDirectoryRoot, IStorageEncryptionProvider encryptionProvider)
2 {
3 if (storage == null) throw new ArgumentNullException("storage");
4
5 int retriesLeft = MaxRetries;
6 while (true)
7 {
8 // work around - attempt to write a file in the folder to determine whether delayed io
9 // needs to be processed
10 // since only a limited number of retries will be attempted, some extreme cases may
11 // still fail if file io is deferred long enough.
12 // while it's still possible that the deferred IO is still pending when the item that failed
13 // to be added is removed by the cleanup code, thus making the cleanup fail,
14 // the item should eventually be removed (by the original removal)
15 try
16 {
17 storage.CreateDirectory(itemDirectoryRoot);
18
19 // try to write a file
20 // if there is a pending operation or the folder is gone, this should find the problem
21 // before writing an actual field is attempted
22 using (IsolatedStorageFileStream fileStream =
23 new IsolatedStorageFileStream(itemDirectoryRoot + @"\sanity-check.txt", FileMode.Create, FileAccess.Write, FileShare.None, storage))
24 { }
25 break;
26 }
27 catch (UnauthorizedAccessException)
28 {
29 // there are probably pending operations on the directory - retry if allowed
30 if (retriesLeft-- > 0)
31 {
32 Thread.Sleep(RetryDelayInMilliseconds);
33 continue;
34 }
35
36 throw;
37 }
38 catch (DirectoryNotFoundException)
39 {
40 // a pending deletion on the directory was processed before creating the file
41 // but after attempting to create it - retry if allowed
42 if (retriesLeft-- > 0)
43 {
44 Thread.Sleep(RetryDelayInMilliseconds);
45 continue;
46 }
47
48 throw;
49 }
50 }
51
52 keyField = new IsolatedStorageCacheItemField(storage, "Key", itemDirectoryRoot, encryptionProvider);
53 valueField = new IsolatedStorageCacheItemField(storage, "Val", itemDirectoryRoot, encryptionProvider);
54 scavengingPriorityField = new IsolatedStorageCacheItemField(storage, "ScPr", itemDirectoryRoot, encryptionProvider);
55 refreshActionField = new IsolatedStorageCacheItemField(storage, "RA", itemDirectoryRoot, encryptionProvider);
56 expirationsField = new IsolatedStorageCacheItemField(storage, "Exp", itemDirectoryRoot, encryptionProvider);
57 lastAccessedField = new IsolatedStorageCacheItemField(storage, "LA", itemDirectoryRoot, encryptionProvider);
58 }
至于IsolatedStorageFile这个类.我查了一下,这个类在sl或wp中用的比较多.这个类更具体的信息,各位看官自行谷歌吧.
七.性能记数器

这个就没什么说的了,就是将各种缓存的操作次数记录下来,包括成功的次数与失败的次数.CachingInstrumentationProvider类里包含了13个EnterpriseLibraryPerformanceCounter类型的计数器.这种计数器其实是系统计数器PerformanceCounter类型的封装.这13个计数器分别为:命中/秒,总命中数,未命中/秒,总未命中数,命中比,缓存总记问数,过期数/秒,总过期数,轮询清除/秒,总轮询清除数,缓存项总数,更新缓存项/秒,更新缓存项总数.更加具体的信息,各位看官自行谷歌吧.
至此,缓存组件的分析告一段落了.我感觉缓存组件比上一篇写到的对象创建模块要好的很多,代码结构清晰,职责分明.里面涉及的众多技术运用,如多线程,锁,性能,面向接口编程等也较为合理,算的上是一个学习的样本.
文章的最后放上一段我最喜欢的一句话吧:
“设计软件有两种策略,一是做的非常的简单,以至于明显没有缺陷。二是做的非常的复杂,以至于没有明显的缺陷。” – C.A.R. Hoare