很多时候,我们会需要这样的功能:
1,甲告诉乙,你去监控一个值,每个一秒钟向我汇报一次
2,甲告诉乙,我现在要去做别的事情了,委托给你一件事情,5秒钟后你去做
要实现上面的场景,就需要一种功能。这种功能,有时候我们称为订阅。
就像我们在邮局订了杂志一样,每个月,邮局都会主动把杂志给我们送过来,不用我们每个月都去给邮局打个电话,告诉他给我送杂志来。
因为这个功能很有用,也很常用,所以我决定把它加入到这个框架里面来。
在编码之前,我们当然要好好想一想,应该如何去做。
现在,需求已经很明确了,但是还缺少分析。下面,就从对需求的分析开始。
从上面的需求里面,我们可以整理得到下面的内容:
1,这个功能要以异步的方式实现。比如,当乙去监控某个值的时候,甲还可以继续做他自己的事情。
2,乙应该可以做很多事情,不仅仅是只能监控一个数值,比如可以监控很多数值,并且对每个数值,向甲汇报的频率都不一样。
3,乙不能占用太多资源。因为从需求里面可以看出来,乙虽然在监控,但是大部分时间还是在等待,因此,乙没有理由占用过多系统资源。
现在,可以对订阅功能进行设计了。
下面是第一次的设计方案:
1,既然乙是异步于甲在工作,那当然要开个新的线程了。
2,为了让乙支持多任务。给乙配置一个任务队列。把要做的事情都打包成任务,放入队列里面。对每个任务包,应该包括下面一些内容:
被调用的频率;
要做的事情;
3,在新开的线程里面,做一个死循环,每次循环的时候,都等待一个很短的时间,然后遍历整个任务队列,对每一个任务包进行判断,如果距离上次被调用的时间间隔超过了设置的调用频率,就调用一次,执行他里面包含的任务功能,比如读取某个数值,然后发送给甲。
这是一个最初步的方案了。先不着急实现,因为这里面有些问题,还挺大。
问题1,每一次循环,都是等一个相同的时间。比如有两个任务,一个频率是1秒,一个是0.1秒。为了满足0.1的需求,那么每次循环的时候,等待的时间就不能高于0.1秒。这样,对1秒的任务,在每次调用之前,我们都进行了10次判断,其中有9次都是无效的。如果能把这些判断省下来,会好很多。
问题2,假如有100个任务,频率都是0.1秒,但是对于其中任何一个任务,调用执行的时间都是1ms。这样,每休息0.1秒,系统就要工作0.1秒。然后再休息0.1秒,再工作0.1秒…………反映到甲那里,就是0.2秒更新了一次!!这就错了。而且,在乙工作的那0.1秒里面,它几乎抢占了所有的系统资源,这是甲不允许的。
下面,是针对方案一的问题的解决思路:
对于问题2,如果能把那100个任务排成一个真正的队列,让他们按次序循环工作,比如ABCDE......ABCDE.....的次序,当任务A再次被调用的时候,时间间隔正好是0.1秒。又因为每次都执行一个任务,因此乙也不会占用多少系统资源。而且,这样一来,也不用去判断A当前是否需要调用,这就把问题1也解决了。
按照上面的思路,下面提出方案二:
对乙里面的任务队列进行排序,让它变成一个调度队列。每次从这个队列里面取出来的任务,都是要被执行的。
那么,怎么搞出这个调度队列呢?
先做一个假设吧。假如我有10个任务要执行。其中7个的频率是1秒(取名为A1,A2...A6,A7),4个的频率是2秒(取名为B1,B2,B3,B4),3个频率是3秒(取名为C1,C2,C3)。再假设每个任务的执行时间都很短(这是合理的。如果一个任务的执行时间远大于它的更新频率……那就是——有病!!)。如果我有一个下面的调度队列,那就再好不过了。
A1,A2,A3,A4,A5,A6,A7,B1,B2,C1
A1,A2,A3,A4,A5,A6,A7,B3,B4,C2
A1,A2,A3,A4,A5,A6,A7,B1,B2,C3
A1,A2,A3,A4,A5,A6,A7,B3,B4,C1
A1,A2,A3,A4,A5,A6,A7,B1,B2,C2
A1,A2,A3,A4,A5,A6,A7,B3,B4,C3
.....
现在,我让乙每次循环先休息1秒,然后执行10个任务,也就是上面的一行。可以看到。对于A,B,C这三种不同频率的任务,都在更新频率的时间间隔内得到了执行。
为了让上面的叙述更明显,下面给出没有进行排列的调度队列:
A1,A2,A3,A4,A5,A6,A7,B1,B2,B3,B4,C1,C2,C3
A1,A2,A3,A4,A5,A6,A7
A1,A2,A3,A4,A5,A6,A7
A1,A2,A3,A4,A5,A6,A7,B1,B2,B3,B4,C1,C2,C3
A1,A2,A3,A4,A5,A6,A7
A1,A2,A3,A4,A5,A6,A7
...
这可以明显的看出来,没有排序的队列,效率很低。
下面给出方案二的设计:
1,使用System.Threading.Timer。这个Timer线程,可以设定每隔一定的时间间隔就执行一次,正好适合当前的场景。
2,对任务队列进行处理,生成调度队列。让系统的负荷总是保持一个平稳的水平,也有利于提高系统的稳定性。
下面是调度队列的设计:
1,引入一个最小调度单位,它是Timer执行的时间间隔。任何一个任务的频率,都是这个调度单位的整数倍,不是整数倍的进行四舍五入,计为pollTime,即调度的频率等级。
2,对每个任务包,设计一个currentClass属性,作为当前调度的次序。它在1到pollTime之间循环。每隔最小调度单位时间变化一次,具体变化方式为currentClass加1,如果超过originalClass,就置为1。
3,每次Timer执行的时候,只要从队列里面把currentClass为1的任务包取出来就可以了。
4,因为调度队列里面的顺序,只有在添加或者删除任务包的时候才会改变。因此,每次添加或删除一个任务包,就要对调度队列进行一次排序。而在调用的过程中,不用再进行排序。
下面是调度队列里面任务的设计:
1,包含一个MyTask成员,封装了要执行的操作
2,包含一个pollTime成员,记录更新频率。1表示循环一次就执行一次,2表示循环两次更新一次……依次类推。
3,包含一个currentClass属性,表示任务当前所处的循环次序。当值为1的时候,表示任务需要被执行。比如pollTime是3,表示循环3次执行一次。currentClass是2,表示当前是在3次循环间隔里的第2次循环。currentClass值每次循环都自动加1,当currentClass的值超过pollTime的时候,就设置为1。
4,初始的时候,对任务进行排序,确定currentClass和pollTime。
好了,说了这么多,该上代码了。
下面先给出调度任务类的核心代码。这个类是SubscribeItem
先是内部成员
private object key;
private MyTask task;
private int polltime;
private int currentClass = 1;
private bool pollOnce = false;
构造函数:
public SubscribeItem(MyTask atask, object key, int pollTime, bool Once)
{
this.task = atask;//任务包
this.key = key;//相当于任务包ID,方便在调度队列里面的存取
this.polltime = Convert.ToInt32(pollTime / ConfigProvider.GetInstance.MinPollTime);//对调度间隔进行简单的取整处理,确定pollTime
this.pollOnce = Once;//表示是否只调度一次
}
设定CurrentClass
public int CurrentClass
{
get
{
return this.currentClass;
}
set
{
currentClass = value;
if (currentClass < 1)
{
currentClass += polltime;
}
else if (currentClass > this.polltime)
{
currentClass -= this.polltime;
}
}
}
SubscribeItem的主要部分就是上面这些了。
下面来说说调度功能的实现,在类Subscriber里面。
依然先是内部成员:
private static Subscriber myself;//应用了单件设计模式
private Hashtable subitems = Hashtable.Synchronized(new Hashtable());//调度队列
private object polllock = new object();
private bool isStart = false;
private System.Threading.Timer pollerTimer; //keep a handle to the timer, so it can be disposed.
添加调度任务的方法:
public void Add(SubscribeItem item)
{
lock(polllock)
{
if (!isStart)
{
isStart = true;
//启动一个Timer线程
pollerTimer = new System.Threading.Timer(new TimerCallback(StartPolling),null,0,ConfigProvider.GetInstance.MinPollTime);
}
//添加到队列里面
if (subitems.Contains(item.Key))
{
subitem.Remove(item.Key);
}
subitem.Add(item.Key,item);
//设置CurrentClass
SetCurrentClass(item);
}
}
设置CurrentClass
private void SetCurrentClass(SubscribeItem item)
{
//声明一个临时的list,用来保存要进行重新设置CurrentClass的任务
List<SubscribeItem> al = new List<SubscribeItem>();
//is not empty , continue
if (subitems.Count > 0)
{
//只有跟当前新变更的任务pollTime相同的任务,才需要修改CurrentClass,其他的不需要修改。
foreach (SubscribeItem si in subitems.Values)
{
//取出与当前任务同pollTime的任务
if (si.PollTime == item.PollTime)
{
al.Add(si);
}
}
if (al.Count > 0)
{
//重新设置这些任务的CurrentClass
SubscribeItem si;
for(int i = 0; i < al.Count ; i++)
{
si = al[i];
si.CurrentClass = i % item.PollTime + 1;
}
}
}
}
下面,我解释一下,为什么只改变跟当前添加的任务pollTime一样的任务的CurrentClass,而其他的不需要重新设置。
假设原始数据队列中pollTime为1的数据有n1个,pollTime为2的数据有n2个,……,pollTime为n的数据有nn个,则平均每次需要更新的数据量m为:
m = n1 + n2/2 +……+ nt/t +……+ nn/n
现在,pollTime为t的数据个数变成了nt'个,则相应的m'为:
m' = n1 + n2/2 +…nt…+ nt'/t +……+ nn/n
对比这两个公式,我们就能看出来,只修改pollTime为t的那些任务就可以了。
下面是删除任务的方法:
public void Remove(SubscribeItem item)
{
lock(polllock)
{
if (subitems.Contains(item.Key))
{
subitems.Remove(item.Key);
//删除一个任务,也要更新CurrentClass
SetCurrentClass(item);
}
//如果队列空,就停止Timer线程
if (subitems.Count == 0)
{
pollerTimer.Dispose();
isStart = false;
}
}
}
Timer线程调用的回调方法:
private void StartPolling(object param)
{
//获取当前要执行的任务
List<SubscribeItem> list = GetCurrentItems();
foreach(SubscribeItem item in list)
{
try
{
//取出任务,放入线程池
MyThreadPool.GetInstance.Dispatch(item.Task);
//如果只执行一次,就删除它。
if (item.Once)
{
Remove(item);
}
}
catch(System.Exception ex)
{
log.Error(ex.Message);
}
}
}
获取当前要执行的任务的方法:
private List<SubscribeItem> GetCurrentItems()
{
List<SubscribeItem> list = new List<SubscribeItem>();
foreach(SubscribeItem item in subitems.Values)
{
//把CurrentClass为1的任务取出来
if (item.CurrentClass == 1)
{
list.Add(item);
}
item.CurrentClass--;
}
return list;
}
到此,订阅功能的主要内容就介绍完了。
下面举个例子,说明如何使用。
在DemoTask的Execute方法里,可以这样使用:
public void Execute(ThreadPool tp)
{
log.Debug("got msg and disposed!");
//这是一个例子:解析数据包,添加订阅
if (buffer[0] == 's')
{
log.Debug("subscriber!");
//创建一个要订阅的任务
SubscribeItem item = SubscribeItem(new PollTask(),"suber",500,false);
//把这个任务添加到调度模块里面即可。调度功能会自动启动。
Subscriber.GetInstance.Add(item);
}
if (buffer[0] == 'u')
{
log.Debug("Unsubscriber!");
//删除一个订阅,也要创建一个任务,其中suber字符串是这个调度任务的key
SubscribeItem item = SubscribeItem(new PollTask(),"suber",500,false);
//删除任务后,调度功能会自动停止,释放系统资源。
Subscriber.GetInstance.Remove(item);
}
}