【IT168 管理开发】
支持顺序访问的Singleton
虽然单例的实例本身是唯一的,但是并不能保证用户对于单件实例内部属性的使用也是顺序的。应用中之所以设置单件对象,一般是为了做某些集中的处理,当然也常常为了对某些集中的信息进行维护。由于单件模式中的这个唯一实例是静态的,那么在实际使用上它虽然本身是个实例对象,但是对于其属性数据的分散引用,在并发条件下的操作具有集中静态(static)对象操作的相似限制。
以最简单的计数器为例,由于许多行业会要求编号的连续,那么如何在业务逻辑层保证即便有多个客户程序可以发起请求的情况下,仍然保证单件实例属性的变更不重复、不跳跃,这就需要为其增加串行化的顺序访问控制。为了达到串行化的访问控制效果可以采用两个方式完成:有阻塞方式、无阻塞方式。
其中有阻塞方式采用简单的锁控制方式或令牌征用方式,对于单件实例的属性修改完全通过类似lock的机制来过滤,保证每次仅有一个客户程序可以获得属性数据并对其操作。在工程上,这种方式的实现主要面向修改请求分布较为平均的情况。一般而言,面向的是处理与前端用户交互有关的业务逻辑单件环境下,实现本身相对简单;无阻塞的方式则是采用类似UDP协议的one way方式,保证单件环境尽最大可能的接受修改请求,请求的模式既可以是“细水长流”方式的调用,也可以是瞬间“排山倒海”的批量调用。尤其在B2B情形下,某些后端服务需要这种方式的处理——当然工程实现上相对会复杂很多。
对于有阻塞方式仅需要在属性数据的set访问方法上增加一个lock即可——这里笔者略过。下面就无阻塞方式,笔者提供一个示例实现。这里笔者假设一个应用情形如下:
- 某企业有一个仓库系统,由于其企业的专业化生产,所以仅有有限的若干几类产品。
- 仓库系统为了向业务主管提供实时的业务指标,需要随时修改每一类产品的库存总量。
- 由于对每一类产品实例会被许多功能调用,工程实现上为了节省总体内存,通过单件模式方式描述了每一类产品,而汇总数据则是通过修改唯一实例的属性完成。
- 由于入库和出库均采用大型车辆,所以对于库存总量的修改是间歇的批量方式修改单件实例的汇总统计属性。
首先,定义单件类型:
using System;
namespace VisionLogic.DesignPattern.Practice
...{
class Singleton
...{
public static readonly Singleton Instance = new Singleton();
![]()
/**//// <summary>
/// 共享的实例数据
/// </summary>
private int total;
public int Total
...{
get ...{ return total; }
set ...{ total = value; }
}
}
}
示例1:具有共享属性的单件实例
下面定义描述每个处理任务的WorkItem,它们对于单件实例共享属性的操作定义在Process方法中完成:
using System;
namespace VisionLogic.DesignPattern.Practice
...{
/**//// <summary>
/// 每个独立的任务处理对象
/// </summary>
public class WorkItem
...{
private int quantity;
private Singleton instance = Singleton.Instance;
public WorkItem(int quantity) ...{ this.quantity = quantity; }
![]()
/**//// <summary>
/// 每个具体任务的处理逻辑
/// </summary>
public void Process()
...{
try
...{
instance.Total += quantity;
}
catch ...{ }
}
}
}
示例2:描述每个具体处理任务的WorkItem
然后,通过一个WorkQueue保证即便存在批量的任务,也可以尽最大可能的批量“扔”给处理人物队列,WorkQueue内部通过定时触发的调用和Queue保证了客户程序的无阻塞化。
using System;
using System.Collections.Generic;
using System.Threading;
namespace VisionLogic.DesignPattern.Practice
...{
/**//// <summary>
/// 通过队列实现顺序访问机制
/// </summary>
public class WorkQueue
...{
private System.Threading.Timer timer;
private Queue<WorkItem> queue;
private const int DueTime = 1000; // 处理前的预热时间
private const int Interval = 1000; // 触发每次批量处理的时间间隔
![]()
public WorkQueue()
...{
timer = new Timer(Process, null, DueTime, Interval);
queue = new Queue<WorkItem>();
}
![]()
/**//// <summary>
/// 增加处理任务
/// </summary>
/// <param name="workItem"></param>
public void Add(WorkItem workItem)
...{
queue.Enqueue(workItem);
}
![]()
/**//// <summary>
/// 批量的任务队列处理
/// </summary>
private void Process(object state)
...{
lock(this)
...{
if(queue.Count > 0)
...{
// 逐个获得工作任务
while (queue.Count > 0)
...{
WorkItem workItem = queue.Dequeue();
workItem.Process();
}
}
}
}
}
}
示例3:循序访问任务队列 WorkQueue
最后,就可以通过客户程序批量的向工作队列提交处理任务修改单件实例的共享属性:
using System;
using System.Diagnostics;
using VisionLogic.DesignPattern.Practice;
namespace VisionLogic.DesignPattern.Practice.Client
...{
class Program
...{
private const int Seed = 97;
![]()
static void Main(string[] args)
...{
WorkQueue queue = new WorkQueue();
WorkItem workItem;
int quantity;
Random rnd = new Random();
![]()
for (int i = 0; i < 100; i++)
...{
quantity = rnd.Next() % Seed;
workItem = new WorkItem(quantity);
queue.Add(workItem);
}
![]()
System.Threading.Thread.Sleep(2000);
Trace.WriteLine(Singleton.Instance.Total);
}
}
}
示例4:批量提交任务的客户程序
其中有如下几个要点需要说明:
- 增加整型字段DueTime和Interval,通过时钟的定期触发机制保证处理过程的持续自动化,同时减少了采用轮循方式判断是否有待处理任务的系统开销。不过在工程实施上,由于每批地处理本身时间可能很长,因此还需要为时钟对象增加更为复杂的控制,例如:处理超时、批最大处理量控制,等等。
- 在具体的WorkItem处理前面增加了一个内部的异常处理,保证每个独立任务的处理异常不至于扩散到整个处理系统。如果监控上需要了解到失败的任务,或者业务上需要对于失败的处理进行补救处理,那么这个异常处理过程本身就会膨胀得非常复杂。为了不影响整个处理流程的后续操作,笔者建议工程实施上采用统一的“出版——预订”方式来设计异常部分。就像允许客户程序尽最大可能的“扔”过来处理任务,异常处理部分也要做到尽最大可能的“抛”出异常。
例如,可以采用集成Exception Handling Application Block的方式,把异常处理作为它的客户程序,尽最大可能的把错误“抛”给异常处理部分。然后,异常处理模块根据预先定义好的异常处理策略和日志机制,通过对于异常内容的替换和重组,面向监控和补救程序提供工作任务的异常情况。

图:Exception Handling Application Block结构。