Enterprise Library Caching Application Block(CAB)源代码研究之缓存条目清理后台线程篇
在Caching Application Block(文中将简称为CAB)中关于如何处理缓存条目的文章有不少,但对于如何实现的说的不多。也许很多大牛们不屑吧。
从设计思想上来说
移除条目大的方向有两个,一个是主动的调用Move方法;另外一个是通过后台进程的方法。
移除条目大的方向有两个,一个是主动的调用Move方法;另外一个是通过后台进程的方法。
对于主动调用,就不赘述了。下面引出本文所要描述的重点,即后台进程移除缓存条目。
设计到的类总共有如下几个:
//下面几个类完成后台线程的管理
BackgroundScheduler,实现ICacheScavenger接口:后台线程主类,负责管理所有后台线程,后台线程集合存放在线程安全的ProducerConsumerQueue队列中。
ProducerConsumerQueue:线程安全的Queue,Queue中放入实现IQueueMessage接口的对象
IQueueMessage:后台线程类的操作接口,BackgroundScheduler调用这个接口完成实际的工作
StartScavengingMsg,实现IQueueMessage接口,内部拥有BackgroundScheduler的引用
ExpirationTimeoutExpiredMsg,实现IQueueMessage接口,内部拥有BackgroundScheduler的引用
//下面这两个类完成实际的移除工作
ScavengerTask:清理器,用来执行缓存清理工作,主要是当缓存条目到最大条目时候执行清理
ExpirationTask:过期条目清理器,用来实际执行过期条目的删除
//下面是一些其他参与类
ExpirationPollTimer:内部拥有一个计时器,用来定期执行过期缓存条目删除
Cache:调用ICacheScavenger,执行ScavengerTask工作
CacheManagerAssembler:初始化所有后台线程类
从缓存条目移除的角度来看,这几个类之间的实际关系如下:
ScavengerTask与ExpirationTask是两个实际执行缓存条目移除的类。这两个类可以认为是缓存移除的Core类部分,既可以直接调用也可以被包装成后台调用。下面我们来分析CAB如何从后台线程调用这两个类完成缓存条目的清理工作的。
首先看CacheManagerAssembler类,这个类是在CAB被加载的时候执行的,有关的代码如下:(此部分代码在CacheManagerFactoryHelper中,CacheManagerAssembler调用CacheManagerFactoryHelper完成加载过程)
Cache cache = new Cache(backingStore, scavengingPolicy, instrumentationProvider);
ExpirationPollTimer timer = new ExpirationPollTimer();
ExpirationTask expirationTask = CreateExpirationTask(cache, instrumentationProvider);
ScavengerTask scavengerTask = new ScavengerTask(numberToRemoveWhenScavenging, scavengingPolicy, cache, instrumentationProvider);
BackgroundScheduler scheduler = new BackgroundScheduler(expirationTask, scavengerTask, instrumentationProvider);
cache.Initialize(scheduler);
scheduler.Start();
timer.StartPolling(new TimerCallback(scheduler.ExpirationTimeoutExpired), expirationPollFrequencyInSeconds * 1000);
return new CacheManager(cache, scheduler, timer);
这段代码描述了上面涉及到的部分类之间的关系。此处看也许不一定能看懂这些代码,可以在看完本文后重新来阅读这段代码。我们先主要看这段代码的其中一句
timer.StartPolling(new TimerCallback(scheduler.ExpirationTimeoutExpired), expirationPollFrequencyInSeconds * 1000);
这句话的主要意思是在固定的时间间隔中调用scheduler的ExpirationTimeoutExpired方法。而scheduler是BackgroundScheduler 的一个实例。也就是说系统将间隔固定时间执行过期条目的清理。同时这个清理的过程交给后台线程管理类BackgroundScheduler 来执行。这样所有条目的清理任务都交给了BackgroundScheduler 来管理。而ExpirationPollTimer 类负责定期通知BackgroundScheduler 来执行这个过程。
下面我们就隆重的退出CAB后台线程管理类BackgroundScheduler
首先看它的加载与启动:
//定义线程
private Thread inputQueueThread;
//加载
ThreadStart queueReader = new ThreadStart(QueueReader);
inputQueueThread = new Thread(queueReader);
//启动线程
public void Start()
{
running = true;
inputQueueThread.Start();
}
在CacheManagerAssembler类中,有如下代码
BackgroundScheduler scheduler = new BackgroundScheduler(expirationTask, scavengerTask, instrumentationProvider);
cache.Initialize(scheduler);
scheduler.Start();
这两段代码完成了线程管理类在CAB模块加载的时候运行于后台的目的。
下面我们来看BackgroundScheduler 类是如何设计的
BackgroundScheduler 类拥有一个队列ProducerConsumerQueue,这个队列中存放的是所有的要在后台执行任务的对象。所有在后台执行任务的对象都需要实现IQueueMessage接口。
IQueueMessage接口内容如下:
internal interface IQueueMessage
{
void Run();
}
从BackgroundScheduler的加载部分我们可以得知,该线程的回调函数是QueueReader
我们来看这个函数的主要内容:
private void QueueReader()
{
isActive = true;
while (running)
{
IQueueMessage msg = inputQueue.Dequeue() as IQueueMessage;
try
{
if (msg == null)
{
continue;
}
msg.Run();
}
catch (ThreadInterruptedException)
{
}
catch (Exception e)
{
instrumentationProvider.FireCacheFailed(Resources.BackgroundSchedulerProducerConsumerQueueFailure, e);
}
}
isActive = false;
}
从这个代码我们可以看出后台类在不断的轮休去执行队列中的所有任务。从而实现后台线程管理类的大的框架。
上文讲到,ExpirationPollTimer 类负责通知BackgroundScheduler 定期执行过期条目处理任务。它的回调函数是ExpirationTimeoutExpired:
public void ExpirationTimeoutExpired(object notUsed)
{
inputQueue.Enqueue(new ExpirationTimeoutExpiredMsg(this));
}
可以看出,回调函数的主要功能是在队列inputQueue中增加要执行过期条目处理任务的对象,而这个对象将在QueueReader函数中执行实际的操作。
此处要注意,ExpirationPollTimer 类并没有直接回调实际执行任务的对象,而是委托给BackgroundScheduler 来调用。统一了后台线程的管理,这样的设计方法值得学习。
BackgroundScheduler整体框架分析完,我们来分析每一个任务的具体实现方法。
两个消息类的实现方法相同,以其中的StartScavengingMsg来分析,实现代码如下:
internal class StartScavengingMsg : IQueueMessage
{
private BackgroundScheduler callback;
public StartScavengingMsg(BackgroundScheduler callback)
{
this.callback = callback;
}
public void Run()
{
callback.DoStartScavenging();
}
}
可以看到这个消息类拥有BackgroundScheduler 的引用,又重新把实际执行的任务委托给类BackgroundScheduler 来执行。
internal void DoStartScavenging()
{
lock (ignoredScavengeRequestsCountLock)
{
// This will make the next schedule request to be scheduled even if it may not be necessary; the
// bookkeeping required to make a more accurate decision is more complex and the outcome cannot
// be 100% reliable.
// The lock taken will impact the background scheduler thread only.
ignoredScavengeRequestsCount = 0;
}
scavenger.DoScavenging();
}
此处我们终于看到实际执行任务的scavenger对象了。最终BackgroundScheduler 委托ScavengerTask来执行缓存条目清理任务。通过这样一个系列的包装,ScavengerTask与ExpirationTask这两个实际执行缓存条目移除的类就开始在后台默默的工作了。
呵呵,前面讲了ExpirationTask是通过ExpirationPollTimer来定时调用触发的机制。那么另外一个清理类ScavengerTask是如何来触发的了?
我们回到CacheManagerAssembler的加载过程,
BackgroundScheduler scheduler = new BackgroundScheduler(expirationTask, scavengerTask, instrumentationProvider);
cache.Initialize(scheduler);
BackgroundScheduler scheduler = new BackgroundScheduler(expirationTask, scavengerTask, instrumentationProvider);
cache.Initialize(scheduler);
可知,在初始化时,我们将BackgroundScheduler 实例放入cache中,观察cache的代码可知,它在Add条目的时候调用这个BackgroundScheduler 的实例,Add代码有关部分如下:
if (scavengingPolicy.IsScavengingNeeded(inMemoryCache.Count))
{
cacheScavenger.StartScavenging();
}
public void StartScavenging()
{
// Despite its name, this method will schedule a scavenge task.
// This request will be ignored if the request is superfluous, ie if a previous request
// has not been processed yet and the current request would "fit" on it
bool scheduleRequest = false;
// This lock is required because by now the Cache would have released the lock for the in-memory
// representation and would only have the lock for the specific item (see Cache.Add()).
// The overhead caused by acquiring this lock would be partially offset by avoiding the call to
// ProducerConsumerQueue.Enqueue() in some occasions; how many times this call will be avoided will depend
// on the load: the higher the load the more likely it will be avoided.
lock (ignoredScavengeRequestsCountLock)
{
int currentCount = ignoredScavengeRequestsCount;
scheduleRequest = currentCount == 0;
ignoredScavengeRequestsCount = (currentCount + 1) % this.scavenger.NumberOfItemsToBeScavenged;
}
if (scheduleRequest)
{
inputQueue.Enqueue(new StartScavengingMsg(this));
}
}
可以得知,Cache在加入条目的时候提出清理的请求。而清理请求将执行一个判断,如果超过最大条目则将清理任务放入后台队列中,进行清理。而后台队列的调用过程见上面一小节。
如上,我们分析了CAB模块的清理缓存条目的全部过程。其中我们可以学习它的后台线程类的设计思路,这个思路可以应用在自己系统的后台线程类的实现中。这也是我记录这个学习过程的主要目的。
至于如何判断缓存条目是否过期,如何清理等,不是本文主要的目的。
简单的说,它通过一个策略模式来实现。每一个CacheItem中都包含一个过期处理策略的数组ICacheItemExpiration[],数组中放入该CacheItem过期处理策略,每个策略都实现ICacheItemExpiration接口,CacheItem通过这个接口来判断该CacheItem是否过期。CAB中实现AbsoluteTime(绝对时间过期策略)、SlidingTime(相对时间过期策略)、NeverExpired(永不过期策略)、FileDependency与ExtendedFormatTime等。