服务器端的高性能实现(七)——订阅功能的引入

很多时候,我们会需要这样的功能:
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);
 }
}

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值