借助于有效的自动化垃圾回收机制,.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. sort,wc,uniq 排序与统计命令

    sort命令 语法:sort [parameter] [file or stdin]参数: -f:忽略大小写的差异,例如A和a视为编码相同 -b:忽略最前面的空格符部分 -n:使用"纯数字& ...

  2. Selenium启动Chrome浏览器提示“请停用以开发者模式运行的扩展程序”的解决办法

    安装了python selenium,运行下面代码: 1 from selenium import webdriver 2 3 browser = webdriver.Chrome() 4 brows ...

  3. C语言:整数取值范转及溢出

    short.int.long 是C语言中常用的三种整数类型,分别称为短整型.整型.长整型.在现代操作系统中,short.int.long 的长度分别是 2.4.4 或者 8,它们只能存储有限的数值,当 ...

  4. pip3 pip 安装包 临时更换镜像地址

    在使用pip3或者pip安装某些第三方包的时候,可能会遇到网络原因导致的安装失败. 可以在安装第三方包的时候临时指定镜像地址. 命令: pip3 install 库名 -i 镜像地址 例如:# pip ...

  5. pytest框架运用

    import pytest ''' 运行方式 1. pytest -s test01.py 把print信息打印出来运行用例 2. 通过main运行 前置后置方法 1. 函数级 setup teard ...

  6. [考试总结]noip模拟23

    因为考试过多,所以学校的博客就暂时咕掉了,放到家里来写 不过话说,vscode的markdown编辑器还是真的很好用 先把 \(noip\) 模拟 \(23\) 的总结写了吧.. 俗话说:" ...

  7. VM12升级VM15

    之前一直用的12,现在想要升级为15.主要是为了解决kali操作系统版本兼容问题 打开VM12,点击[帮助]-->[软件更新]--> [检查更新] 发现有VM15,点击[了解详情] VM1 ...

  8. onclick="return doAlert()" onclick="return false"

    return false不是取bai消事件冒泡,而是取消"du浏览器默认行为".比如一个链接zhi<a href="http://zhidao.baidu.com& ...

  9. mysql orderby limit 翻页数据重复的问题

    在mysql中我们通常会采用limit来进行翻页查询,比如limit(0,10)表示列出第一页的10条数据,limit(10,10)表示列出第二页.但是,当limit遇到order by的时候,可能会 ...

  10. sqli-labs 靶场笔记

    sqli-labs 靶场笔记 冲冲冲,好好学习 2020年1月12号 先来点铺垫 分类: 按照请求方法分类: 1.Get型注入:注入参数在get的参数里. 2.POST型注入:注入参数在POST的参数 ...