【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结构。
池化特征的Singleton – N系统
上述讨论虽然针对最原始单件模式进行了展开,但是实际工程使用中往往可能会提出如下需要:
出于性能的考虑,需要所有的业务逻辑最大可能的基于已有实例。但是为了隔离每个实例,保证每个实例的独立处理,同时又不会因为同时存在过多实例影响节点的总体内存使用,需要允许最多在 N 个实例中共享它们的引用。
其实,很多开发人员在以往的开发中都或多或少的接触过具有上面特征的对象,例如:连接池、线程池等具有池化特性的对象。结合原始单件模式的定义,这里笔者把这类问题需要的结果定义为Singleton – N对象类型。由于 N 的引入,这里其实带来了上面单件“1”模式对象类型所没有讨论到的方面——实例的有效回收。对于单件“1”模式,一般而言不考虑对于这个唯一实例的回收,稍微复杂点的应用也就类似上文增加一个自动更新机制,但是这个“独苗”还是不会被处理掉。不过对于Singleton – N,为了保证其有效的随着吞吐量弹性的使用系统资源,对于企业关键的应用最好为其增加回收机制。
对于按需生成不多于N个实例的部分相对比较容易,需要改造的部分仅包括上面静态Instance属性的get部分。但是对于回收如何选择回收时机,相对而言要复杂很多:回收的过早,会导致客户程序后续调用中没有实例引用可用;回收的太晚,甚至于必须要等到客户实例被GC时同时把引用实例的句柄回收似乎又过晚。尤其是客户程序如果是把这个实例的引用作为某个静态属性使用时,那么一旦实例被分配出去就很难再收回。这里,笔者参考ADO.NET对于连接池的使用,默认将客户端应用假设为不可信任的,只能通过显示Open和Close操作有限的N个实例。同时,考虑到Singleton – N的某些实例将会在客户程序长期引用(甚至是静态引用),因此还需要为Singleton – N外面加个外壳。考虑减少内存占用的问题,外壳本身以单件方式对外提供可能的操作实例。
内部共享资源的管理设计
对于共享资源的使用本身其实是安全的维护每个实体对象的状态,此外还要根据系统的吞吐状态动态的把多余的实例提交给GC。
首先,设计每个工作实体对象:
using System;
using System.Diagnostics;
namespace VisionLogic.DesignPattern.Practice
...{
/**//// <summary>
/// 共享对象的状态
/// </summary>
enum Status
...{
InUse,
NotInUse
}
![]()
/**//// <summary>
/// 共享对象接口
/// </summary>
public interface IWorkItem
...{
void Process();
void Release();
}
![]()
/**//// <summary>
/// 共享对象
/// </summary>
class WorkItem : IWorkItem
...{
private Status status;
private DateTime lastestUsed;
private static readonly DateTime DefaultLatestUsed = new DateTime(2020, 12, 31);
![]()
internal WorkItem()
...{
status = Status.NotInUse;
lastestUsed = DefaultLatestUsed;
}
![]()
/**//// <summary>
/// 工作对象的处理逻辑
/// </summary>
public void Process()
...{
lastestUsed = DateTime.Now;
Trace.WriteLine("do something ...");
}
![]()
/**//// <summary>
/// 显式的引用申请
/// </summary>
public void Allocate()
...{
lastestUsed = DateTime.Now;
status = Status.InUse;
}
![]()
/**//// <summary>
/// 显式的引用释放
/// </summary>
public void Release()
...{
lastestUsed = DateTime.Now;
status = Status.NotInUse;
}
![]()
/**//// <summary>
/// 描述当前共享工作对象的状态
/// </summary>
public Status Status
...{
get ...{ return status; }
set ...{ status = value; }
}
![]()
/**//// <summary>
/// 获得当前共享工作对象最近被访问过的时间
/// </summary>
public DateTime LatestUsed
...{
get ...{ return lastestUsed; }
}
![]()
}
}
![]()
示例5:具有最近访问状态信息的工作实体对象
其中,为了屏蔽外界代码对于工作实体的直接构造,这里把工作实体的构造器访问控制设置为internal,仅仅将引用释放和处理两个方法设计为公共的IWorkItem接口提供给客户端程序使用。为了便于资源的回收,为每个工作对象增加了LastestUsed属性,表示该示例对象最近被访问的时间。
接着,设计内层对于工作实体对象管理的Singleton – N实体:
using System;
using System.Collections.Generic;
namespace VisionLogic.DesignPattern.Practice
...{
/**//// <summary>
/// 内层对于共享工作的管理实体
/// </summary>
static class WorkItemManager
...{
private const int MaxInstances = 5; // 最大的池化数量
private const int RelaseInterval = 1000; // 最长允许的闲置时间
private const int DueTime = 1000; // 时钟的预热时间
private static List<WorkItem> list; // 共享实体集合
private static System.Threading.Timer timer;// 回收控制时钟
![]()
static WorkItemManager()
...{
list = new List<WorkItem>();
timer = new System.Threading.Timer(Release, null, DueTime, RelaseInterval);
}
![]()
/**//// <summary>
/// 申请和动态创建共享实体的过程
/// </summary>
/// <returns></returns>
internal static IWorkItem Allocate()
...{
lock (list)
...{
// 首先使用已有的实例
if(list.Count > 0)
for(int i=0; i<list.Count; i++)
if(list[i].Status == Status.NotInUse)
...{
list[i].Allocate();
return list[i];
}
![]()
// 如果可以创建新的实例
if (list.Count < MaxInstances)
...{
WorkItem workItem = new WorkItem();
workItem.Allocate();
list.Add(workItem);
![]()
return workItem;
}
![]()
// 不能继续申请
return null;
}
}
![]()
/**//// <summary>
/// 获得实际运行的实体数量
/// </summary>
internal static int ActiveInstances
...{
get
...{
lock (list)
...{
if (list.Count == 0)
return 0;
![]()
int count = 0;
for (int i = 0; i < list.Count; i++)
if (list[i].Status == Status.InUse)
count++;
![]()
return count;
}
}
}
![]()
/**//// <summary>
/// 获得池中已经构造出的实体数量
/// </summary>
internal static int Instances
...{
get
...{
lock (list)
...{
return list.Count;
}
}
}
![]()
/**//// <summary>
/// 根据时钟的触发,动态释放引用实例的方法
/// </summary>
/// <param name="state"></param>
private static void Release(object state)
...{
lock (list)
...{
if (list.Count <= 0)
return;
![]()
// 检查是否有需要销毁的实例,并把他们的序号保存在releaseList
List<int> releaseList = new List<int>();
for (int i = 0; i < list.Count; i++)
...{
if (list[i].Status == Status.InUse)
continue;
else
...{
TimeSpan span = DateTime.Now - list[i].LatestUsed;
if (span.TotalMilliseconds > RelaseInterval)
releaseList.Add(i);
}
}
![]()
if (releaseList.Count <= 0)
return;
releaseList.Reverse();
![]()
// 根据releaseList 逐个释放超时的对象
for (int i = 0; i < releaseList.Count; i++)
...{
int index = releaseList[i];
WorkItem workItem = list[index];
list.Remove(workItem);
workItem = null;
}
}
}
}
}
示例6:内层Singleton – N管理对象
其中,Allocate方法向外提供访问实例。为了最大可能的提高资源的使用率,这里对于实例的申请采取了尽可能重用的办法,只有实例集合中再也找不到空闲实例时才会继续生成一个新的实例。由于资源的释放被设计为内部管理对象的内在自动机制,因此Release方法的访问控制为私有的。同时,考虑到运行监控的要求,WorkItemManager还通过静态的ActiveInstances属性和Instances属性向外提供实际的在运行实例数和池中总计的实例数。
外层自由引用对象的设计
外层的设计就是一个简单的单件模式类型,不过同时处于降低客户程序对于工作实例耦合性和访问安全性的考虑。外层Singleton类型仅仅对外提供IWorkItem接口的引用,而非实体的WorkItem类型。
using System;
namespace VisionLogic.DesignPattern.Practice
...{
/**//// <summary>
/// 单件模式的外层引用对象
/// </summary>
public class Singleton
...{
public static readonly Singleton Instance = new Singleton();
![]()
/**//// <summary>
/// 代理获得工作实体的引用
/// </summary>
/// <returns></returns>
public IWorkItem Allocate() ...{ return WorkItemManager.Allocate();}
![]()
/**//// <summary>
/// 对外提供监控信息
/// </summary>
public static int ActiveInstances ...{ get ... { return WorkItemManager.ActiveInstances; } }
public static int Instances ...{ get ...{ return WorkItemManager.Instances; } }
}
}
示例7:外层引用对象的设计
外层客户程序的使用
最后,通过提供一个客户程序验证前面Singleton – N池的有效性:
using System;
using System.Diagnostics;
using VisionLogic.DesignPattern.Practice;
namespace VisionLogic.DesignPattern.Practice.Client
...{
class Program
...{
static void Main(string[] args)
...{
// 通过外层引用对象获得可能的工作实例访问资格
Singleton[] instances = new Singleton[10];
for (int i = 0; i < instances.Length; i++)
instances[i] = Singleton.Instance;
![]()
// 申请和使用工作实例
IWorkItem[] items = new IWorkItem[instances.Length];
for (int i = 0; i < instances.Length; i++)
items[i] = instances[i].Allocate();
Trace.WriteLine(Singleton.Instances);
![]()
for (int i = 0; i < instances.Length; i++)
if (items[i] != null)
...{
items[i].Process();
items[i].Release();
}
![]()
// 等待Singleton - N的自动资源回收
System.Threading.Thread.Sleep(6000);
Trace.WriteLine(Singleton.Instances);
}
}
}
![]()
结果:
5
do something ...
do something ...
do something ...
do something ...
do something ...
0
从结果不难看出,外层的10个引用其实仅仅有5个获得了实际的工作对象,而且实际的工作对象也随着后续处理工作的消失自动的被内层共享资源管理逻辑清理了。
其他说明
后续的进一步Singleton – N可能根据项目的规模和复杂度,需要有更为繁琐的生命期控制,例如COM+中对于组件池的管理规则和Remoting中具体组件的生命期管理。