编程篇》已经涉及到了对象池模型的大部分核心接口和类型。对象池模型其实是很简单的,不过其中有一些为了提升性能而刻意为之的实现细节倒是值得我们关注。总的来说,对象池模型由三个核心对象构成,它们分别是表示对象池的ObjectPool<T>对象、对象值提供者的ObjectPoolProvider对象,已及控制池化对象创建与释放行为的IPooledObjectPolicy<T>对象,我们先来介绍最后一个对象。

目录
一、 IPooledObjectPolicy<T>

二、ObjectPool<T>
     DefaultObjectPool<T>
     DisposableObjectPool<T>

三、ObjectPoolProvider

一、 IPooledObjectPolicy<T>

我们在《编程篇》已经说过,表示池化对象策略的IPooledObjectPolicy<T>对象不仅仅帮助我们创建对象,还可以帮助我们执行一些对象回归对象池之前所需的回收操作,对象最终能否回到对象池中也受它的控制。如下面的代码片段所示,IPooledObjectPolicy<T>接口定义了两个方法,Create方法用来创建池化对象,对象回归前需要执行的操作体现在Return方法上,该方法的返回值决定了指定的对象是否应该回归对象池。抽象类PooledObjectPolicy<T>实现了该接口,我们一般将它作为自定义策略类型的基类。

public interface IPooledObjectPolicy<T>
{
T Create();
bool Return(T obj);
} public abstract class PooledObjectPolicy<T> : IPooledObjectPolicy<T>
{
protected PooledObjectPolicy(){} public abstract T Create();
public abstract bool Return(T obj);
}

我们默认使用的是如下这个DefaultPooledObjectPolicy<T>类型,由于它直接通过反射来创建池化对象,所以要求泛型参数T必须有一个公共的默认无参构造函数。它的Return方法直接返回True,意味着提供的对象可以被无限制地复用。

public class DefaultPooledObjectPolicy<T> : PooledObjectPolicy<T> where T: class, new()
{
public override T Create() => Activator.CreateInstance<T>();
public override bool Return(T obj) => true;
}

二、ObjectPool<T>

对象池通过ObjectPool<T>对象表示。如下面的代码片段所示,ObjectPool<T>是一个抽象类,池化对象通过Get方法提供给我们,我们在使用完之后调用Return方法将其释放到对象池中以供后续复用。

public abstract class ObjectPool<T> where T: class
{
protected ObjectPool(){} public abstract T Get();
public abstract void Return(T obj);
}

DefaultObjectPool<T>

我们默认使用的对象池体现为一个DefaultObjectPool<T>对象,由于针对对象池的绝大部分实现就体现这个类型中,所以它也是本节重点讲述的内容。我们在前面一节已经说过,对象池具有固定的大小,并且默认的大小为处理器个数的2倍。我们假设对象池的大小为N,那么DefaultObjectPool<T>对象会如下图所示的方式使用一个单一对象和一个长度为N-1的数组来存放由它提供的N个对象。

如下面的代码片段所示,DefaultObjectPool<T>使用字段_firstItem用来存放第一个池化对象,余下的则存放在_items字段表示的数组中。值得注意的是,这个数组的元素类型并非池化对象的类型T,而是一个封装了池化对象的结构体ObjectWrapper。如果该数组元素类型改为引用类型T,那么当我们对某个元素进行复制的时候,运行时会进行类型校验(要求指定对象类型派生于T),无形之中带来了一定的性能损失(值类型数组就不需求进行派生类型的校验)。我们在前面提到过,对象池中存在一些性能优化的细节,这就是其中之一。

public class DefaultObjectPool<T> : ObjectPool<T> where T : class
{
private protected T _firstItem;
private protected readonly ObjectWrapper[] _items;

private protected struct ObjectWrapper
{
public T Element;
}
}

DefaultObjectPool<T>类型定义了如下两个构造函数。我们在创建一个DefaultObjectPool<T>对象的时候会提供一个IPooledObjectPolicy<T>对象并指定对象池的大小。对象池的大小默认设置为处理器数量的2倍体现在第一个构造函数重载中。如果指定的是一个DefaultPooledObjectPolicy<T>对象,表示默认池化对象策略的_isDefaultPolicy字段被设置成True。因为DefaultPooledObjectPolicy<T>对象的Return方法总是返回True,并且没有任何具体的操作,所以在将对象释放回对象池的时候就不需要调用Return方法了,这是第二个性能优化的细节。

public class DefaultObjectPool<T> : ObjectPool<T> where T : class
{
private protected T _firstItem;
private protected readonly ObjectWrapper[] _items;
private protected readonly IPooledObjectPolicy<T> _policy;
private protected readonly bool _isDefaultPolicy;
private protected readonly PooledObjectPolicy<T> _fastPolicy; public DefaultObjectPool(IPooledObjectPolicy<T> policy) : this(policy, Environment.ProcessorCount * 2)
{} public DefaultObjectPool(IPooledObjectPolicy<T> policy, int maximumRetained)
{
_policy = policy ;
_fastPolicy = policy as PooledObjectPolicy<T>;
_isDefaultPolicy = IsDefaultPolicy();
_items = new ObjectWrapper[maximumRetained - 1]; bool IsDefaultPolicy()
{
var type = policy.GetType();
return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(DefaultPooledObjectPolicy<>);
}
} [MethodImpl(MethodImplOptions.NoInlining)]
private T Create() => _fastPolicy?.Create() ?? _policy.Create();
}

从第二个构造函数的定义可以看出,指定的IPooledObjectPolicy<T>对象除了会赋值给_policy字段之外,如果提供的是一个PooledObjectPolicy<T>对象,该对象还会同时赋值给另一个名为_fastPolicy的字段。在进行池化对象的提取和释放时,_fastPolicy字段表示的池化对象策略会优先选用,这个逻辑体现在Create方法上。因为调用类型的方法比调用接口方法具有更好的性能(所以该字段才会命名为_fastPolicy),这是第三个性能优化的细节。这个细节还告诉我们在自定义池化对象策略的时候,最好将PooledObjectPolicy<T>作为基类,而不是直接实现IPooledObjectPolicy<T>接口。

如下所示的是重写的Get和Return方法的定义。用于提供池化对象的Get方法很简单,它会采用原子操作使用Null将_firstItem字段表示的对象“替换”下来,如果该字段不为Null,那么将其作为返回的对象,反之它会遍历数组的每个ObjectWrapper对象,并使用Null将其封装的对象“替换”下来,第一个成功替换下来的对象将作为返回值。如果所有ObjectWrapper对象封装的对象都为Null,意味着所有对象都被“借出”或者尚未创建,此时返回创建的新对象了。

public class DefaultObjectPool<T> : ObjectPool<T> where T : class
{
public override T Get()
{
var item = _firstItem;
if (item == null || Interlocked.CompareExchange(ref _firstItem, null, item) != item)
{
var items = _items;
for (var i = 0; i < items.Length; i++)
{
item = items[i].Element;
if (item != null && Interlocked.CompareExchange( ref items[i].Element, null, item) == item)
{
return item;
}
}
item = Create();
}
return item;
} public override void Return(T obj)
{
if (_isDefaultPolicy || (_fastPolicy?.Return(obj) ?? _policy.Return(obj)))
{
if (_firstItem != null || Interlocked.CompareExchange(ref _firstItem, obj, null) != null)
{
var items = _items;
for (var i = 0; i < items.Length && Interlocked.CompareExchange( ref items[i].Element, obj, null) != null; ++i)
{}
}
}
}

}

将对象释放会对象池的Return方法也很好理解。首先它需要判断指定的对象能否释放会对象池中,如果使用的是默认的池化对象策略,答案是肯定的,否则只能通过调用IPooledObjectPolicy<T>对象的Return方法来判断。从代码片段可以看出,这里依然会优先选择_fastPolicy字段表示的PooledObjectPolicy<T>对象以获得更好的性能。

在确定指定的对象可以释放回对象之后,如果_firstItem字段为Null,Return方法会采用原子操作使用指定的对象将其“替换”下来。如果该字段不为Null或者原子替换失败,该方法会便利数组的每个ObjectWrapper对象,并采用原子操作将它们封装的空引用替换成指定的对象。整个方法会在某个原子替换操作成功或者整个便利过程结束之后返回。

DefaultObjectPool<T>之所有使用一个数组附加一个单一对象来存储池化对象,是因为针对单一字段的读写比针对数组元素的读写具有更好的性能。从上面给出的代码可以看出,不论是Get还是Return方法,优先选择的都是_firstItem字段。如果池化对象的使用率不高,基本上使用的都会是该字段存储的对象,那么此时的性能是最高的。

DisposableObjectPool<T>

通过前面的示例演示我们知道,当池化对象类型实现了IDisposable接口的情况下,如果某个对象在回归对象池的时候,对象池已满,该对象将被丢弃。与此同时,被丢弃对象的Dispose方法将立即被调用。但是这种现象并没有在DefaultObjectPool<T>类型的代码中体现出来,这是为什么呢?实际上DefaultObjectPool<T>还有如下这个名为DisposableObjectPool<T>的派生类。如代码片段可以看出,表示池化对象类型的泛型参数T要求实现IDisposable接口。如果池化对象类型实现了IDisposable接口,通过默认ObjectPoolProvider对象创建的对象池就是一个DisposableObjectPool<T>对象。

internal sealed class DisposableObjectPool<T> : DefaultObjectPool<T>, IDisposable where T : class
{
private volatile bool _isDisposed;
public DisposableObjectPool(IPooledObjectPolicy<T> policy) : base(policy)
{} public DisposableObjectPool(IPooledObjectPolicy<T> policy, int maximumRetained) : base(policy, maximumRetained)
{} public override T Get()
{
if (_isDisposed)
{
throw new ObjectDisposedException(GetType().Name);
}
return base.Get();
} public override void Return(T obj)
{
if (_isDisposed || !ReturnCore(obj))
{
DisposeItem(obj);
}
} private bool ReturnCore(T obj)
{
bool returnedToPool = false;
if (_isDefaultPolicy || (_fastPolicy?.Return(obj) ?? _policy.Return(obj)))
{
if (_firstItem == null && Interlocked.CompareExchange(ref _firstItem, obj, null) == null)
{
returnedToPool = true;
}
else
{
var items = _items;
for (var i = 0; i < items.Length && !(returnedTooPool = Interlocked.CompareExchange(ref items[i].Element, obj, null) == null); i++)
{}
}
}
return returnedTooPool;
} public void Dispose()
{
_isDisposed = true;
DisposeItem(_firstItem);
_firstItem = null; ObjectWrapper[] items = _items;
for (var i = 0; i < items.Length; i++)
{
DisposeItem(items[i].Element);
items[i].Element = null;
}
} private void DisposeItem(T item)
{
if (item is IDisposable disposable)
{
disposable.Dispose();
}
}
}

从上面代码片段可以看出,DisposableObjectPool<T>自身类型也实现了IDisposable接口,它会在Dispose方法中调用目前对象池中的每个对象的Dispose方法。用于提供池化对象的Get方法除了会验证自身的Disposed状态之外,并没有特别之处。当对象未能成功回归对象池,通过调用该对象的Dispose方法将其释放的操作体现在重写的Return方法中。

三、ObjectPoolProvider

表示对象池的ObjectPool<T>对象是通过ObjectPoolProvider提供的。如下面的代码片段所示,抽象类ObjectPoolProvider定义了两个重载的Create<T>方法,抽象方法需要指定具体的池化对象策略。另一个重载由于采用默认的池化对象策略,所以要求对象类型具有一个默认无参构造函数。

public abstract class ObjectPoolProvider
{
public ObjectPool<T> Create<T>() where T : class, new() => Create<T>(new DefaultPooledObjectPolicy<T>());
public abstract ObjectPool<T> Create<T>(IPooledObjectPolicy<T> policy) where T : class;
}

在前面的示例演示中,我们使用的是如下这个DefaultObjectPoolProvider类型。如代码片段所示,DefaultObjectPoolProvider派生于抽象类ObjectPoolProvider,在重写的Create<T>方法中,它会根据泛型参数T是否实现IDisposable接口分别创建DisposableObjectPool<T>和DefaultObjectPool<T>对象。

public class DefaultObjectPoolProvider : ObjectPoolProvider
{
public int MaximumRetained { get; set; } = Environment.ProcessorCount * 2;
public override ObjectPool<T> Create<T>(IPooledObjectPolicy<T> policy) => typeof(IDisposable).IsAssignableFrom(typeof(T))
? new DisposableObjectPool<T>(policy, MaximumRetained) : new DefaultObjectPool<T>(policy, MaximumRetained);
}

DefaultObjectPoolProvider类型定义了一个标识对象池大小的MaximumRetained属性,采用处理器数量的两倍作为默认容量也体现在这里。这个属性并非只读,所以我们可以利用它根据具体需求调整提供对象池的大小。在ASP.NET应用中,我们基本上都会采用依赖注入的方式利用注入的ObjectPoolProvider对象来创建针对具体类型的对象池。我们在《编程篇》还演示了另一种创建对象池的方式,那就是直接调用ObjectPool类型的静态Create<T>方法,该方法的实现体现在如下所示的代码片段中。

public static class ObjectPool
{
public static ObjectPool<T> Create<T>(IPooledObjectPolicy<T> policy) where T: class, new()
=> new DefaultObjectPoolProvider().Create<T>(policy ?? new DefaultPooledObjectPolicy<T>());
}

到目前为止,我们已经将整个对象池的设计模型进行了完整的介绍。总得来说,这是一个简单、高效并且具有可扩展性的对象池框架,该模型涉及的几个核心接口和类型体现在如下图所示的UML中。

对象池在 .NET (Core)中的应用[1]: 编程篇
对象池在 .NET (Core)中的应用[2]: 设计篇
对象池在 .NET (Core)中的应用[3]: 扩展篇

对象池在 .NET (Core)中的应用[2]: 设计篇的更多相关文章

  1. 对象池在 .NET (Core)中的应用[1]: 编程体验

    借助于有效的自动化垃圾回收机制,.NET让开发人员不在关心对象的生命周期,但实际上很多性能问题都来源于GC.并不说.NET的GC有什么问题,而是对象生命周期的跟踪和管理本身是需要成本的,不论交给应用还 ...

  2. 对象池----unity中应用

    对象池应用在unity中能减少资源消耗,节省内存空间具体原理不再赘述. 以下是他的操作步骤:(注意:对象池中应用到了栈或对队列!) 1).先建立一个(怪物)物体   mMonster; 2).再建立一 ...

  3. 【JVM】Java 8 中的常量池、字符串池、包装类对象池

    1 - 引言 2 - 常量池 2.1 你真的懂 Java的“字面量”和“常量”吗? 2.2 常量和静态/运行时常量池有什么关系?什么是常量池? 2.3 字节码下的常量池以及常量池的加载机制 2.4 是 ...

  4. Apache Commons-pool实现对象池(包括带key对象池)

    Commons-pool是一个apache开源组织下的众多项目的一个.其被广泛地整合到众多需要对象池功能的项目中. 官网:http://commons.apache.org/proper/common ...

  5. Netty轻量级对象池实现分析

    什么是对象池技术?对象池应用在哪些地方? 对象池其实就是缓存一些对象从而避免大量创建同一个类型的对象,类似线程池的概念.对象池缓存了一些已经创建好的对象,避免需要时才创建对象,同时限制了实例的个数.池 ...

  6. 在Asp.Net Core中集成Kafka

    在我们的业务中,我们通常需要在自己的业务子系统之间相互发送消息,一端去发送消息另一端去消费当前消息,这就涉及到使用消息队列MQ的一些内容,消息队列成熟的框架有多种,这里你可以读这篇文章来了解这些MQ的 ...

  7. java对象池commons-pool-1.6详解(一)

    自己的项目中用到了 对象池 commons-pool: package com.sankuai.qcs.regulation.protocol.client; import com.dianping. ...

  8. Java对象池

    单例模式是限制了一个类只能有一个实例,对象池模式则是限制一个类实例的个数.对象池类就像是一个对象管理员,它以Static列表(也就是装对象的池子)的形式存存储某个实例数受限的类的实例,每一个实例还要加 ...

  9. Python中小整数对象池和大整数对象池

    1. 小整数对象池 整数在程序中的使用非常广泛,Python为了优化速度,使用了小整数对象池, 避免为整数频繁申请和销毁内存空间. Python 对小整数的定义是 [-5, 256] 这些整数对象是提 ...

随机推荐

  1. C语言:源代码兼容WINDOWS LINUX

    使用库函数之前,应该用#include引入对应的头文件.这种以#号开头的命令称为预处理命令.C语言源文件要经过编译.链接才能生成可执行程序:1) 编译(Compile)会将源文件(.c文件)转换为目标 ...

  2. Day1 Markdown学习!

    Markdown学习 标题 一级标题:# (空格)+内容 二级标题:##(空格)+内容 同理可支持到六级标题 字体 Hello,World! 两边两个** 加粗 Hello,World! 两边一个* ...

  3. sublime安装emmet

    1,先安装package install 2,然后安装emmet,安装完后会自动load加载Pyv8,如果加载不成功只能手动下载(详情可以百度).

  4. jquery性能优化建议-上篇

    一.注意定义jQuery变量的时候添加var关键字这个不仅仅是jQuery,所有javascript开发过程中,都需要注意,请一定不要定义成如下:$loading = $('#loading'); / ...

  5. mybatis-4-Mapper映射文件

    Mapper映射文件 映射文件的Mapper标签包含标签 1.CDUS增删改查 2.参数处理 (1)直接传入参数 单个参数 //传入当个参数 public Employee getEmployeeBy ...

  6. Jenkins 进阶篇 - 参数化构建

    我们在构建任务时经常会遇到这样的情景,一个任务配置好了以后,在后面的构建过程中,又会修改一些配置.例如,我们构建项目的代码可能是拉取指定的分支或者是Tag进行构建,又或者是在构建是需要指定特定的运行平 ...

  7. 【搜索】单词方阵 luogu-1101

    题目描述 给一n×n的字母方阵,内可能蕴含多个"yizhong"单词.单词在方阵中是沿着同一方向连续摆放的.摆放可沿着8个方向的任一方向,同一单词摆放时不再改变方向,单词与单词之间 ...

  8. [考试总结]noip模拟14

    咕掉了好长时间,现在终于回来了.. 这次考试炸裂得很完蛋,直接分数上天. \(T1\) 本来想打一个记忆化搜索,然而老是通过不了样例,然后就挂了,只剩下了垃圾的 \(30pts\) 部分分数. 然而到 ...

  9. jvm源码解读--02 Array<u1>* tags = MetadataFactory::new_writeable_array<u1>(loader_data, length, 0, CHECK_NULL); 函数引入的jvm内存分配解析

    current路径: #0 Array<unsigned char>::operator new (size=8, loader_data=0x7fd4c802e868, length=8 ...

  10. LinkedHashMap 与 LRUcache

    LRU 缓存介绍 我们平时总会有一个电话本记录所有朋友的电话,但是,如果有朋友经常联系,那些朋友的电话号码不用翻电话本我们也能记住,但是,如果长时间没有联系了,要再次联系那位朋友的时候,我们又不得不求 ...