对象池在 .NET (Core)中的应用[2]: 设计篇
《编程篇》已经涉及到了对象池模型的大部分核心接口和类型。对象池模型其实是很简单的,不过其中有一些为了提升性能而刻意为之的实现细节倒是值得我们关注。总的来说,对象池模型由三个核心对象构成,它们分别是表示对象池的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]: 设计篇的更多相关文章
- 对象池在 .NET (Core)中的应用[1]: 编程体验
借助于有效的自动化垃圾回收机制,.NET让开发人员不在关心对象的生命周期,但实际上很多性能问题都来源于GC.并不说.NET的GC有什么问题,而是对象生命周期的跟踪和管理本身是需要成本的,不论交给应用还 ...
- 对象池----unity中应用
对象池应用在unity中能减少资源消耗,节省内存空间具体原理不再赘述. 以下是他的操作步骤:(注意:对象池中应用到了栈或对队列!) 1).先建立一个(怪物)物体 mMonster; 2).再建立一 ...
- 【JVM】Java 8 中的常量池、字符串池、包装类对象池
1 - 引言 2 - 常量池 2.1 你真的懂 Java的“字面量”和“常量”吗? 2.2 常量和静态/运行时常量池有什么关系?什么是常量池? 2.3 字节码下的常量池以及常量池的加载机制 2.4 是 ...
- Apache Commons-pool实现对象池(包括带key对象池)
Commons-pool是一个apache开源组织下的众多项目的一个.其被广泛地整合到众多需要对象池功能的项目中. 官网:http://commons.apache.org/proper/common ...
- Netty轻量级对象池实现分析
什么是对象池技术?对象池应用在哪些地方? 对象池其实就是缓存一些对象从而避免大量创建同一个类型的对象,类似线程池的概念.对象池缓存了一些已经创建好的对象,避免需要时才创建对象,同时限制了实例的个数.池 ...
- 在Asp.Net Core中集成Kafka
在我们的业务中,我们通常需要在自己的业务子系统之间相互发送消息,一端去发送消息另一端去消费当前消息,这就涉及到使用消息队列MQ的一些内容,消息队列成熟的框架有多种,这里你可以读这篇文章来了解这些MQ的 ...
- java对象池commons-pool-1.6详解(一)
自己的项目中用到了 对象池 commons-pool: package com.sankuai.qcs.regulation.protocol.client; import com.dianping. ...
- Java对象池
单例模式是限制了一个类只能有一个实例,对象池模式则是限制一个类实例的个数.对象池类就像是一个对象管理员,它以Static列表(也就是装对象的池子)的形式存存储某个实例数受限的类的实例,每一个实例还要加 ...
- Python中小整数对象池和大整数对象池
1. 小整数对象池 整数在程序中的使用非常广泛,Python为了优化速度,使用了小整数对象池, 避免为整数频繁申请和销毁内存空间. Python 对小整数的定义是 [-5, 256] 这些整数对象是提 ...
随机推荐
- leetcode 数组分成和相等的三个部分
题目: 给你一个整数数组 A,只有可以将其划分为三个和相等的非空部分时才返回 true,否则返回 false. 形式上,如果可以找出索引 i+1 < j 且满足 (A[0] + A[1] + . ...
- ROS2学习之旅(20)——创建一个动作消息
本文用来自定义一个动作消息类型. 以下命令用来创建一个工作空间并建立一个功能包: mkdir -p action_ws/src cd action_ws/src ros2 pkg create act ...
- Django基础-02篇 Models的属性与字段
1.models字段类型 AutoField():一个IntegerField,根据可用ID自动递增.如果没指定主键,就创建它自动设置为主键. IntegerField():一个整数: FloatFi ...
- C语言:条件编译
假如现在要开发一个C语言程序,让它输出红色的文字,并且要求跨平台,在 Windows 和 Linux 下都能运行,怎么办呢?这个程序的难点在于,不同平台下控制文字颜色的代码不一样,我们必须要能够识别出 ...
- Quick BI的复杂系统为例:那些年,我们一起做过的性能优化
背景 一直以来,性能都是技术层面不可避开的话题,尤其在中大型复杂项目中.犹如汽车整车性能,追求极速的同时,还要保障舒适性和实用性,而在汽车制造的每个环节.零件整合情况.发动机调校等等,都会最终影响用户 ...
- SpringBoot整合Shiro实现权限控制
目录 1.SpringBoot整合Shiro 1.1.shiro简介 1.2.代码的具体实现 1.2.1.Maven的配置 1.2.2.整合需要实现的类 1.2.3.项目结构 1.2.4.ShiroC ...
- [源码解析] 深度学习分布式训练框架 horovod (18) --- kubeflow tf-operator
[源码解析] 深度学习分布式训练框架 horovod (18) --- kubeflow tf-operator 目录 [源码解析] 深度学习分布式训练框架 horovod (18) --- kube ...
- Leetcode春季打卡活动 第二题:206. 反转链表
Leetcode春季打卡活动 第二题:206. 反转链表 206. 反转链表 Talk is cheap . Show me the code . /** * Definition for singl ...
- MySQL检查与性能优化示例脚本
最近在玩python,为了熟悉一下python,写了个mysql的检查与性能优化建议的脚本. 虽然,真的只能算是一个半成残次品.也拿出来现眼一下. 不过对于初学者来说,还是有一定的参考价值的.比如说如 ...
- 【贪心+排序】凌乱的yyy / 线段覆盖 luogu-1803
题目描述 现在各大oj上有n个比赛,每个比赛的开始.结束的时间点是知道的. yyy认为,参加越多的比赛,noip就能考的越好(假的) 所以,他想知道他最多能参加几个比赛. 由于yyy是蒟蒻,如果要参加 ...