借助于有效的自动化垃圾回收机制,.NET让开发人员不在关心对象的生命周期,但实际上很多性能问题都来源于GC。并不说.NET的GC有什么问题,而是对象生命周期的跟踪和管理本身是需要成本的,不论交给应用还是框架来做,都会对性能造成影响。在一些对性能比较敏感的应用中,我们可以通过对象复用的方式避免垃圾对象的产生,进而避免GC因对象回收导致的性能损失。对象池是对象复用的一种常用的方式。

.NET提供了一个简单高效的对象池框架,并使用在ASP.NET自身框架中。这个对象池狂框架由“Microsoft.Extensions.ObjectPool”这个NuGet包提供,我们可以通过添加这个NuGet包它引入我们的应用中。接下来我们就通过一些简单的示例来演示一下对象池的基本编程模式。

一、对象的借与还

和绝大部分的对象池编程方式一样,当我们需要消费某个对象的时候,我们不会直接创建它,而是选择从对象池中“借出”一个对象。一般来说,如果对象池为空,或者现有的对象都正在被使用,它会自动帮助我们完成对象的创建。借出的对象不再使用的时候,我们需要及时将其“归还”到对象池中以供后续复用。我们在使用.NET的对象池框架时,主要会使用如下这个ObjectPool<T>类型,针对池化对象的借与还体现在它的GetReturn方法中。

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

我们接下来利用一个简单的控制台程序来演示对象池的基本编程模式。在添加了针对“Microsoft.Extensions.ObjectPool”这个NuGet包的引用之后,我们定义了如下这个FoobarService类型来表示希望池化复用的服务对象。如代码片段所示,FoobarService具有一个自增整数表示Id属性作为每个实例的唯一标识,静态字段_latestId标识当前分发的最后一个标识。

public class FoobarService
{
internal static int _latestId;
public int Id { get; }
public FoobarService() => Id = Interlocked.Increment(ref _latestId);
}

通过对象池的方式来使用FoobarService对象体现在如下的代码片段中。我们通过调用ObjectPool类型的静态方法Create<FoobarService>方法得到针对FoobarService类型的对象池,这是一个ObjectPool<FoobarService>对象。针对单个FoobarService对象的使用体现在本地方法ExecuteAsync中。如代码片段所示,我们调用ObjectPool<FoobarService>对象的Get方法从对象池中借出一个Foobar对象。为了确定对象是否真的被复用,我们在控制台上打印出对象的标识。我们通过延迟1秒钟模拟针对服务对象的长时间使用,并在最后通过调用ObjectPool<FoobarService>对象的Return方法将借出的对象释放到对象池中。

class Program
{
static async Task Main()
{
var objectPool = ObjectPool.Create<FoobarService>();
while (true)
{
Console.Write("Used services: ");
await Task.WhenAll(Enumerable.Range(1, 3).Select(_ => ExecuteAsync()));
Console.Write("\n");
}
async Task ExecuteAsync()
{
var service = objectPool.Get();
try
{
Console.Write($"{service.Id}; ");
await Task.Delay(1000);
}
finally
{
objectPool.Return(service);
}
}
}
}

在Main方法中,我们构建了一个无限循环,并在每次迭代中并行执行ExecuteAsync方法三次。演示实例运行之后会在控制台上输出如下所示的结果,可以看出每轮迭代使用的三个对象都是一样的。每次迭代,它们从对象池中被借出,使用完之后又回到池中供下一次迭代使用。

二、依赖注入

我们知道依赖注入是已经成为 .NET Core的基本编程模式,针对对象池的编程最好也采用这样的编程方式。如果采用依赖注入,容器提供的并不是代表对象池的ObjectPool<T>对象,而是一个ObjectPoolProvider对象。顾名思义, ObjectPoolProvider对象作为对象池的提供者,用来提供针对指定对象类型的ObjectPool<T>对象。

.NET提供的大部分框架都提供了针对IServiceCollection接口的扩展方法来注册相应的服务,但是对象池框架并没有定义这样的扩展方法,所以我们需要采用原始的方式来完成针对ObjectPoolProvider的注册。如下面的代码片段所示,在创建出ServiceCollection对象之后,我们通过调用AddSingleton扩展方法注册了ObjectPoolProvider的默认实现类型DefaultObjectPoolProvider

class Program
{
static async Task Main()
{
var objectPool = new ServiceCollection().AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>()
.BuildServiceProvider()
.GetRequiredService<ObjectPoolProvider>()
.Create<FoobarService>();

}
}

在利用ServiceCollection对象创建出代表依赖注入容器的IServiceProvider对象之后,我们利用它提取出ObjectPoolProvider对象,并通过调用其Create<T>方法得到表示对象池的ObjectPool<FoobarService>对象。改动的程序执行之后同样会在控制台输出如上图所示的结果。

三、池化对象策略

通过前面的实例演示可以看出,对象池在默认情况下会帮助我们完成对象的创建工作。我们可以想得到,它会在对象池无可用对象的时候会调用默认的构造函数来创建提供的对象。如果池化对象类型没有默认的构造函数呢?或者我们希望执行一些初始化操作呢?

在另一方面,当不在使用的对象被归还到对象池之前,很有可能会执行一些释放性质的操作(比如集合对象在归还之前应该被清空)。还有一种可能是对象有可能不能再次复用(比如它内部维护了一个处于错误状态并无法恢复的网络连接),那么它就不能被释放会对象池。上述的这些需求都可以通过IPooledObjectPolicy<T>接口表示的池化对象策略来解决。

同样以我们演示实例中使用的FoobarService类型,如果并不希望用户直接调用构造函数来创建对应的实例,所以我们按照如下的方式将其构造函数改为私有,并定义了一个静态的工厂方法Create来创建FoobarService对象。当FoobarService类型失去了默认的无参构造函数之后,我们演示的程序将无法编译。

public class FoobarService
{
internal static int _latestId;
public int Id { get; }
private FoobarService() => Id = Interlocked.Increment(ref _latestId);
public static FoobarService Create() => new FoobarService();
}

为了解决这个问题,我们为FoobarService类型定义一个代表池化对象策略的FoobarPolicy类型。如代码片段所示,FoobarPolicy类型实现了IPooledObjectPolicy<FoobarService>接口,实现的Create方法通过调用FoobarSerivice类型的静态同名方法完成针对对象的创建。另一个方法Return可以用来执行一些对象归还前的释放操作,它的返回值表示该对象还能否回到池中供后续使用。由于FoobarService对象可以被无限次复用,所以实现的Return方法直接返回True。

public class FoobarPolicy : IPooledObjectPolicy<FoobarService>
{
public FoobarService Create() => FoobarService.Create();
public bool Return(FoobarService obj) => true;
}

在调用ObjectPoolProvider对象的Create<T>方法针对指定的类型创建对应的对象池的时候,我们将一个IPooledObjectPolicy<T>对象作为参数,创建的对象池将会根据该对象定义的策略来创建和释放对象。

class Program
{
static async Task Main()
{
var objectPool = new ServiceCollection().AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>()
.BuildServiceProvider()
.GetRequiredService<ObjectPoolProvider>()
.Create(new FoobarPolicy());

}
}

四、对象池的大小

对象池容纳对象的数量总归是有限的,默认情况下它的大小为当前机器处理器数量的2倍,这一点可以通过一个简单的实例来验证一下。如下面的代码片段所示,我们将演示程序中每次迭代并发执行ExecuteAsync方法的数量设置为当前机器处理器数量的2倍,并将最后一次创建的FoobarService对象的ID打印出来。为了避免控制台上的无效输出,我们将ExecuteAsync方法中的控制台输出代码移除。

class Program
{
static async Task Main()
{
var objectPool = new ServiceCollection().AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>()
.BuildServiceProvider()
.GetRequiredService<ObjectPoolProvider>()
.Create(new FoobarPolicy());
var poolSize = Environment.ProcessorCount * 2;
while (true)
{
while (true)
{
await Task.WhenAll(Enumerable.Range(1, poolSize).Select(_ => ExecuteAsync()));
Console.WriteLine($"Last service: {FoobarService._latestId}");
}
} async Task ExecuteAsync()
{
var service = objectPool.Get();
try
{
await Task.Delay(1000);
}
finally
{
objectPool.Return(service);
}
}
}
}

上面这个演示实例表达的意思是:对象池的大小和对象消费率刚好是一致的。在这种情况下,消费的每一个对象都是从对象池中提取出来,并且能够成功还回去,那么对象的创建数量就是对象池的大小。下图所示的是演示程序运行之后再控制台上的输出结果,整个应用的生命周期范围内一共只会有16个对象被创建出来,因为我当前机器的处理器数量为8。

如果对象池的大小为当前机器处理器数量的2倍,那么我们倘若将对象的消费率提高,意味着池化的对象将无法满足消费需求,新的对象将持续被创建出来。为了验证我们的想法,我们按照如下的方式将每次迭代执行任务的数量加1。

class Program
{
static async Task Main()
{
var objectPool = new ServiceCollection().AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>()
.BuildServiceProvider()
.GetRequiredService<ObjectPoolProvider>()
.Create(new FoobarPolicy());
var poolSize = Environment.ProcessorCount * 2;
while (true)
{
while (true)
{
await Task.WhenAll(Enumerable.Range(1, poolSize + 1)
.Select(_ => ExecuteAsync()));
Console.WriteLine($"Last service: {FoobarService._latestId}");
}
}

}
}

再次运行改动后的程序,我们会在控制台上看到如下图所示的输出结果。由于每次迭代针对对象的需求量是17,但是对象池只能提供16个对象,所以每次迭代都必须额外创建一个新的对象。

五、对象的释放

由于对象池容纳的对象数量是有限的,如果现有的所有对象已经被提取出来,它会提供一个新创建的对象。从另一方面讲,我们从对象池得到的对象在不需要的时候总是会还回去,但是对象池可能容不下那么多对象,它只能将其丢弃,被丢弃的对象将最终被GC回收。如果对象类型实现了IDisposable接口,在它不能回到对象池的情况下,它的Dispose方法应该被立即执行。

为了验证不能正常回归对象池的对象能否被及时释放,我们再次对演示的程序作相应的修改。我们让FoobarService类型实现IDisposable接口,并在实现的Dispose方法中将自身ID输出到控制台上。然后我们按照如下的方式以每次迭代并发量高于对象池大小的方式消费对象。

class Program
{
static async Task Main()
{
var objectPool = new ServiceCollection().AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>()
.BuildServiceProvider()
.GetRequiredService<ObjectPoolProvider>()
.Create(new FoobarPolicy()); while (true)
{
Console.Write("Disposed services:");
await Task.WhenAll(Enumerable.Range(1, Environment.ProcessorCount * 2 + 3).Select(_ => ExecuteAsync()));
Console.Write("\n");
} async Task ExecuteAsync()
{
var service = objectPool.Get();
try
{
await Task.Delay(1000);
}
finally
{
objectPool.Return(service);
}
}
}
} public class FoobarService: IDisposable
{
internal static int _latestId;
public int Id { get; }
private FoobarService() => Id = Interlocked.Increment(ref _latestId);
public static FoobarService Create() => new FoobarService();
public void Dispose() => Console.Write($"{Id}; ");
}

演示程序运行之后会在控制台上输出如下图所示的结果,可以看出对于每次迭代消费的19个对象,只有16个能够正常回归对象池,有三个将被丢弃并最终被GC回收。由于这样的对象将不能被复用,它的Dispose方法会被调用,我们定义其中的释放操作得以被及时执行。

对象池在 .NET (Core)中的应用[1]: 编程体验的更多相关文章

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

    <编程篇>已经涉及到了对象池模型的大部分核心接口和类型.对象池模型其实是很简单的,不过其中有一些为了提升性能而刻意为之的实现细节倒是值得我们关注.总的来说,对象池模型由三个核心对象构成,它 ...

  2. [译]如何在ASP.NET Core中实现面向切面编程(AOP)

    原文地址:ASPECT ORIENTED PROGRAMMING USING PROXIES IN ASP.NET CORE 原文作者:ZANID HAYTAM 译文地址:如何在ASP.NET Cor ...

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

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

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

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

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

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

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

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

  7. Java对象池

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

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

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

  9. Java对象池示例

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

随机推荐

  1. XCTF simple-unpacked

    一.查壳 是UPX的壳,拖入IDA,发现很多函数无法反编译也无法查看 二.骚操作 将那个文件放入记事本,ctrl+F搜索flag. 找到了. 实际上,是需要专门的UPX脱壳工具或者手工来脱壳的,我目前 ...

  2. vue(18)路由懒加载

    什么是路由懒加载 官方的解释: 当打包构建应用时,JavaScript 包会变得非常大,影响页面加载. 如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更 ...

  3. C语言:统计字符个数及种类

    #include <stdio.h> int main(){ char c; //用户输入的字符 int shu=0;//字符总数 int letters=0, // 字母数目 space ...

  4. 两人团队项目-石家庄地铁查询系统(web版)

    大二上学期做过只有两号线的地铁查询系统,但是只能在控制台操作.这一次将线路加到了六条,并且要求web实现,下面简述一下设计思路和具体代码实现: 1.数据库建表 于我自己习惯而言,我写javaweb项目 ...

  5. Leetcode3.无重复字符的最长子串——简洁易懂

    > 简洁易懂讲清原理,讲不清你来打我~ 输入字符串,找到无重复.最长.子串,输出长度 ![在这里插入图片描述](https://img-blog.csdnimg.cn/c0565c943c654 ...

  6. 达梦数据库(DM8)大规模并行集群MPP 2节点安装部署

    达梦数据库大规模并行集群MPP 2节点安装部署   1.环境准备   os 数据库版本 ip mpp角色 centos7.x86 DM8 192.168.30.100 mpp1 centos7.x86 ...

  7. 就这?一篇文章让你读懂 Spring 事务

    什么是事务 ▲ 百度百科 概括来讲,事务是一个由有限操作集合组成的逻辑单元.事务操作包含两个目的,数据一致以及操作隔离.数据一致是指事务提交时保证事务内的所有操作都成功完成,并且更改永久生效:事务回滚 ...

  8. 开发工具IDE从入门到爱不释手(一)项目初始配置

    前言 版本:IDE 2019.2.3 JDK:1.8 一.字体 快捷键:Ctrl+Alt+S  ;打开Settings,一般系统配置都在这里 输入font,需要修改字体有三处 Apperance:ID ...

  9. [SqlServer] 理解数据库中的数据页结构

    这边文章,我将会带你深入分析数据库中 数据页 的结构.通过这篇文章的学习,你将掌握如下知识点: 1. 查看一个 表/索引 占用了多少了页. 2. 查看某一页中存储了什么的数据. 3. 验证在数据库中用 ...

  10. Tomcat PUT方法任意写文件漏洞(CVE-2017-12615)

    Apache Tomcat 7.0.0~7.0.79 直接发送以下数据包即可在Web根目录写入shell: PUT /1.jsp/ HTTP/1.1 Host: 192.168.49.2:8080 A ...