通过 ASP.NET Core 中的 ObjectPool 重用对象
作者:Günther Foidl、 Steve Gordon 和 Samson Amaugo
注意
此版本不是本文的最新版本。 有关当前版本,请参阅本文的 .NET 9 版本。
警告
此版本的 ASP.NET Core 不再受支持。 有关详细信息,请参阅 .NET 和 .NET Core 支持策略。 有关当前版本,请参阅本文的 .NET 9 版本。
Microsoft.Extensions.ObjectPool 是 ASP.NET Core 基础结构的一部分,它支持将一组对象保留在内存中以供重用,而不是允许对对象进行垃圾回收。 Microsoft.Extensions.ObjectPool
中的所有的静态方法和实例方法都是线程安全的。
如果要管理的对象具有以下特征,应用可能希望使用对象池:
- 分配/初始化成本高昂。
- 表示有限资源。
- 可预见地频繁使用。
例如,ASP.NET Core 框架在某些地方使用对象池来重用 StringBuilder 实例。 StringBuilder
分配并管理自己的缓冲区来保存字符数据。 ASP.NET Core 经常使用 StringBuilder
来实现功能,重用这些对象会带来性能优势。
对象池并不总是能提高性能:
- 除非对象的初始化成本很高,否则从池中获取对象通常较慢。
- 在池解除分配之前,池管理的对象无法解除分配。
仅在使用应用或库的真实场景收集性能数据后才使用对象池。
注意:ObjectPool 不限制分配的对象数量,但限制保留的对象数量。
ObjectPool 概念
当使用 DefaultObjectPoolProvider 并且 T
实现 IDisposable
时:
- 未返回到池中的项将被释放。
- 使用 DI 释放池时,将释放池中的所有项。
注意:释放池后:
- 调用
Get
会引发ObjectDisposedException
。 - 调用
Return
会释放给定的项。
重要 ObjectPool
类型和接口:
- ObjectPool<T>:基本对象池抽象。 用于获取和返回对象。
- PooledObjectPolicy<T>:实现后可自定义对象的创建方式及其返回池时的重置方式。 它可以传递到直接构造的对象池中。
- IResettable :返回到对象池时自动重置对象。
可以通过多种方式在应用中使用 ObjectPool:
- 实例化池。
- 在依赖项注入 (DI) 中将池注册为实例。
- 在 DI 中注册
ObjectPoolProvider<>
并将其用作工厂。
如何使用 ObjectPool
调用 Get 获取对象,调用 Return 返回对象。 不必返回每个对象。 如果某个对象未返回,系统将对其进行垃圾回收。
ObjectPool 示例
下面的代码:
- 将
ObjectPoolProvider
添加到依赖项注入 (DI) 容器。 - 实现
IResettable
接口,以在返回到对象池时自动清除缓冲区的内容。
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.ObjectPool;
using System.Security.Cryptography;
var builder = WebApplication.CreateBuilder(args);
builder.Services.TryAddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>();
builder.Services.TryAddSingleton<ObjectPool<ReusableBuffer>>(serviceProvider =>
{
var provider = serviceProvider.GetRequiredService<ObjectPoolProvider>();
var policy = new DefaultPooledObjectPolicy<ReusableBuffer>();
return provider.Create(policy);
});
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
// return the SHA256 hash of a word
// https://localhost:7214/hash/SamsonAmaugo
app.MapGet("/hash/{name}", (string name, ObjectPool<ReusableBuffer> bufferPool) =>
{
var buffer = bufferPool.Get();
try
{
// Set the buffer data to the ASCII values of a word
for (var i = 0; i < name.Length; i++)
{
buffer.Data[i] = (byte)name[i];
}
Span<byte> hash = stackalloc byte[32];
SHA256.HashData(buffer.Data.AsSpan(0, name.Length), hash);
return "Hash: " + Convert.ToHexString(hash);
}
finally
{
// Data is automatically reset because this type implemented IResettable
bufferPool.Return(buffer);
}
});
app.Run();
public class ReusableBuffer : IResettable
{
public byte[] Data { get; } = new byte[1024 * 1024]; // 1 MB
public bool TryReset()
{
Array.Clear(Data);
return true;
}
}
注意: 当共用类型 T
未实现 IResettable
时,可以使用自定义 PooledObjectPolicy<T>
在对象返回到池之前重置其状态。
Microsoft.Extensions.ObjectPool 是 ASP.NET Core 基础结构的一部分,它支持将一组对象保留在内存中以供重用,而不是允许对对象进行垃圾回收。 Microsoft.Extensions.ObjectPool
中的所有的静态方法和实例方法都是线程安全的。
如果要管理的对象具有以下特征,应用可能希望使用对象池:
- 分配/初始化成本高昂。
- 表示有限资源。
- 可预见地频繁使用。
例如,ASP.NET Core 框架在某些地方使用对象池来重用 StringBuilder 实例。 StringBuilder
分配并管理自己的缓冲区来保存字符数据。 ASP.NET Core 经常使用 StringBuilder
来实现功能,重用这些对象会带来性能优势。
对象池并不总是能提高性能:
- 除非对象的初始化成本很高,否则从池中获取对象通常较慢。
- 在池解除分配之前,池管理的对象无法解除分配。
仅在使用应用或库的真实场景收集性能数据后才使用对象池。
注意:ObjectPool 不限制分配的对象数量,但限制保留的对象数量。
概念
当使用 DefaultObjectPoolProvider 并且 T
实现 IDisposable
时:
- 未返回到池中的项将被释放。
- 使用 DI 释放池时,将释放池中的所有项。
注意:释放池后:
- 调用
Get
会引发ObjectDisposedException
。 - 调用
Return
会释放给定的项。
重要 ObjectPool
类型和接口:
- ObjectPool<T>:基本对象池抽象。 用于获取和返回对象。
- PooledObjectPolicy<T>:实现后可自定义对象的创建方式及其返回池时的重置方式。 它可以传递到直接构造的对象池中,或者
- Create:充当工厂来创建对象池。
- IResettable : 返回到对象池时自动重置对象。
可以通过多种方式在应用中使用 ObjectPool:
- 实例化池。
- 在依赖项注入 (DI) 中将池注册为实例。
- 在 DI 中注册
ObjectPoolProvider<>
并将其用作工厂。
如何使用 ObjectPool
调用 Get 获取对象,调用 Return 返回对象。 不必返回每个对象。 如果不返回某个对象,系统将对其进行垃圾回收。
ObjectPool 示例
下面的代码:
- 将
ObjectPoolProvider
添加到依赖项注入 (DI) 容器。 - 向 DI 容器添加并配置
ObjectPool<StringBuilder>
。 - 添加
BirthdayMiddleware
。
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.ObjectPool;
using ObjectPoolSample;
using System.Text;
var builder = WebApplication.CreateBuilder(args);
builder.Services.TryAddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>();
builder.Services.TryAddSingleton<ObjectPool<StringBuilder>>(serviceProvider =>
{
var provider = serviceProvider.GetRequiredService<ObjectPoolProvider>();
var policy = new Microsoft.Extensions.ObjectPool.StringBuilderPooledObjectPolicy();
return provider.Create(policy);
});
builder.Services.AddWebEncoders();
var app = builder.Build();
// Test using /?firstname=Steve&lastName=Gordon&day=28&month=9
app.UseMiddleware<BirthdayMiddleware>();
app.MapGet("/", () => "Hello World!");
app.Run();
下面的代码实现了 BirthdayMiddleware
using System.Text;
using System.Text.Encodings.Web;
using Microsoft.Extensions.ObjectPool;
namespace ObjectPoolSample;
public class BirthdayMiddleware
{
private readonly RequestDelegate _next;
public BirthdayMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context,
ObjectPool<StringBuilder> builderPool)
{
if (context.Request.Query.TryGetValue("firstName", out var firstName) &&
context.Request.Query.TryGetValue("lastName", out var lastName) &&
context.Request.Query.TryGetValue("month", out var month) &&
context.Request.Query.TryGetValue("day", out var day) &&
int.TryParse(month, out var monthOfYear) &&
int.TryParse(day, out var dayOfMonth))
{
var now = DateTime.UtcNow; // Ignoring timezones.
// Request a StringBuilder from the pool.
var stringBuilder = builderPool.Get();
try
{
stringBuilder.Append("Hi ")
.Append(firstName).Append(" ").Append(lastName).Append(". ");
var encoder = context.RequestServices.GetRequiredService<HtmlEncoder>();
if (now.Day == dayOfMonth && now.Month == monthOfYear)
{
stringBuilder.Append("Happy birthday!!!");
var html = encoder.Encode(stringBuilder.ToString());
await context.Response.WriteAsync(html);
}
else
{
var thisYearsBirthday = new DateTime(now.Year, monthOfYear,
dayOfMonth);
int daysUntilBirthday = thisYearsBirthday > now
? (thisYearsBirthday - now).Days
: (thisYearsBirthday.AddYears(1) - now).Days;
stringBuilder.Append("There are ")
.Append(daysUntilBirthday).Append(" days until your birthday!");
var html = encoder.Encode(stringBuilder.ToString());
await context.Response.WriteAsync(html);
}
}
finally // Ensure this runs even if the main code throws.
{
// Return the StringBuilder to the pool.
builderPool.Return(stringBuilder);
}
return;
}
await _next(context);
}
}
Microsoft.Extensions.ObjectPool 是 ASP.NET Core 基础结构的一部分,它支持将一组对象保留在内存中以供重用,而不是允许对对象进行垃圾回收。
如果要管理的对象具有以下特征,你可能希望使用对象池:
- 分配/初始化成本高昂。
- 表示某些有限资源。
- 可预见地频繁使用。
例如,ASP.NET Core 框架在某些地方使用对象池来重用 StringBuilder 实例。 StringBuilder
分配并管理自己的缓冲区来保存字符数据。 ASP.NET Core 经常使用 StringBuilder
来实现功能,重用这些对象会带来性能优势。
对象池并不总是能提高性能:
- 除非对象的初始化成本很高,否则从池中获取对象通常较慢。
- 在池解除分配之前,池管理的对象无法解除分配。
仅在使用应用或库的真实场景收集性能数据后才使用对象池。
警告:ObjectPool
不实现 IDisposable
。 建议不要将其与需要释放的类型一起使用。 ASP.NET Core 3.0 及更高版本中的 ObjectPool
支持 IDisposable
。
注意:ObjectPool 不限制将分配的对象数量,但限制将保留的对象数量。
概念
ObjectPool<T> - 基本对象池抽象。 用于获取和返回对象。
PooledObjectPolicy<T> - 实现后可自定义对象的创建方式及其返回池时的重置方式。 它可以传递到你直接构造的对象池中.... 或者
Create 充当工厂来创建对象池。
可以通过多种方式在应用中使用 ObjectPool:
- 实例化池。
- 在依赖项注入 (DI) 中将池注册为实例。
- 在 DI 中注册
ObjectPoolProvider<>
并将其用作工厂。
如何使用 ObjectPool
调用 Get 获取对象,调用 Return 返回对象。 不必返回每个对象。 如果不返回某个对象,系统将对其进行垃圾回收。
ObjectPool 示例
下面的代码:
- 将
ObjectPoolProvider
添加到依赖项注入 (DI) 容器。 - 向 DI 容器添加并配置
ObjectPool<StringBuilder>
。 - 添加
BirthdayMiddleware
。
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.TryAddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>();
services.TryAddSingleton<ObjectPool<StringBuilder>>(serviceProvider =>
{
var provider = serviceProvider.GetRequiredService<ObjectPoolProvider>();
var policy = new StringBuilderPooledObjectPolicy();
return provider.Create(policy);
});
services.AddWebEncoders();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
// Test using /?firstname=Steve&lastName=Gordon&day=28&month=9
app.UseMiddleware<BirthdayMiddleware>();
}
}
下面的代码实现了 BirthdayMiddleware
public class BirthdayMiddleware
{
private readonly RequestDelegate _next;
public BirthdayMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context,
ObjectPool<StringBuilder> builderPool)
{
if (context.Request.Query.TryGetValue("firstName", out var firstName) &&
context.Request.Query.TryGetValue("lastName", out var lastName) &&
context.Request.Query.TryGetValue("month", out var month) &&
context.Request.Query.TryGetValue("day", out var day) &&
int.TryParse(month, out var monthOfYear) &&
int.TryParse(day, out var dayOfMonth))
{
var now = DateTime.UtcNow; // Ignoring timezones.
// Request a StringBuilder from the pool.
var stringBuilder = builderPool.Get();
try
{
stringBuilder.Append("Hi ")
.Append(firstName).Append(" ").Append(lastName).Append(". ");
var encoder = context.RequestServices.GetRequiredService<HtmlEncoder>();
if (now.Day == dayOfMonth && now.Month == monthOfYear)
{
stringBuilder.Append("Happy birthday!!!");
var html = encoder.Encode(stringBuilder.ToString());
await context.Response.WriteAsync(html);
}
else
{
var thisYearsBirthday = new DateTime(now.Year, monthOfYear,
dayOfMonth);
int daysUntilBirthday = thisYearsBirthday > now
? (thisYearsBirthday - now).Days
: (thisYearsBirthday.AddYears(1) - now).Days;
stringBuilder.Append("There are ")
.Append(daysUntilBirthday).Append(" days until your birthday!");
var html = encoder.Encode(stringBuilder.ToString());
await context.Response.WriteAsync(html);
}
}
finally // Ensure this runs even if the main code throws.
{
// Return the StringBuilder to the pool.
builderPool.Return(stringBuilder);
}
return;
}
await _next(context);
}
}