通过 ASP.NET Core 中的 ObjectPool 重用对象

作者:Günther FoidlSteve GordonSamson Amaugo

注意

此版本不是本文的最新版本。 对于当前版本,请参阅此文的 .NET 8 版本

重要

此信息与预发布产品相关,相应产品在商业发布之前可能会进行重大修改。 Microsoft 对此处提供的信息不提供任何明示或暗示的保证。

对于当前版本,请参阅此文的 .NET 8 版本

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);
    }
}