Reutilização de objeto com ObjectPool no ASP.NET Core

Por Günther Foidl, Steve Gordon e Samson Amaugo

Observação

Esta não é a versão mais recente deste artigo. Para informações sobre a versão vigente, confira a Versão do .NET 8 deste artigo.

Aviso

Esta versão do ASP.NET Core não tem mais suporte. Para obter mais informações, confira .NET e a Política de Suporte do .NET Core. Para informações sobre a versão vigente, confira a Versão do .NET 8 deste artigo.

Importante

Essas informações relacionam-se ao produto de pré-lançamento, que poderá ser substancialmente modificado antes do lançamento comercial. A Microsoft não oferece nenhuma garantia, explícita ou implícita, quanto às informações fornecidas aqui.

Para informações sobre a versão vigente, confira a Versão do .NET 8 deste artigo.

Microsoft.Extensions.ObjectPool faz parte da infraestrutura do ASP.NET Core que dá suporte à manutenção de um grupo de objetos na memória para reutilização, em vez de permitir que os objetos sejam coletados como lixo. Todos os métodos estáticos e de instância em Microsoft.Extensions.ObjectPool são thread-safe.

Talvez os aplicativos queiram usar o pool de objetos se os objetos que estão sendo gerenciados forem:

  • Caro para alocar/inicializar.
  • Representa um recurso limitado.
  • Usado previsivelmente e com frequência.

Por exemplo, a estrutura ASP.NET Core usa o pool de objetos em alguns locais para reutilizar instâncias do StringBuilder. StringBuilder aloca e gerencia seus próprios buffers para armazenar dados de caracteres. O ASP.NET Core usa StringBuilder regularmente para implementar recursos e reutilizá-los oferece um benefício de desempenho.

O pool de objetos nem sempre melhora o desempenho:

  • A menos que o custo de inicialização de um objeto seja alto, geralmente é mais lento obter o objeto do pool.
  • Os objetos gerenciados pelo pool não são desalocados até que o pool seja desalocado.

Use o pool de objetos somente depois de coletar dados de desempenho usando cenários realistas para seu aplicativo ou biblioteca.

OBSERVAÇÃO: o ObjectPool não coloca um limite no número de objetos alocados, coloca um limite no número de objetos que ele retém.

Conceitos de ObjectPool

Quando DefaultObjectPoolProvider é usado e T implementa IDisposable:

  • Os itens que não forem retornados ao pool serão descartados.
  • Quando o pool é descartado pela DI, todos os itens no pool são descartados.

OBSERVAÇÃO: depois que o pool é descartado:

  • Chamar Get gera um ObjectDisposedException.
  • Chamar Return descarta o item especificado.

Tipos e interfaces importantes do ObjectPool:

  • ObjectPool<T>: a abstração básica do pool de objetos. Usado para obter e retornar objetos.
  • PooledObjectPolicy<T>: implemente isso para personalizar como um objeto é criado e como ele é redefinido quando retornado ao pool. Isso pode ser passado para um pool de objetos que é construído diretamente.
  • IResettable: redefine automaticamente o objeto quando retornado para um pool de objetos.

O ObjectPool pode ser usado em um aplicativo de várias maneiras:

  • Instanciando um pool.
  • Registrar um pool na Injeção de dependência(DI) como uma instância.
  • Registrando o ObjectPoolProvider<> na DI e usando-o como uma fábrica.

Como usar ObjectPool

Chame Get para obter um objeto e Return para retornar o objeto . Não há nenhum requisito para retornar todos os objetos. Se um objeto não for retornado, ele será coletado como lixo.

Exemplo de ObjectPool

O seguinte código:

  • Adiciona ObjectPoolProvider ao contêiner de Injeção de dependência(DI).
  • Implementa a interface do IResettable para limpar automaticamente o conteúdo do buffer quando retornado ao pool de objetos.
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;
    }
}

OBSERVAÇÃO: Quando o tipo T em pool não implementa IResettable, um PooledObjectPolicy<T> personalizado pode ser usado para redefinir o estado dos objetos antes que eles sejam retornados ao pool.

Microsoft.Extensions.ObjectPool faz parte da infraestrutura do ASP.NET Core que dá suporte à manutenção de um grupo de objetos na memória para reutilização, em vez de permitir que os objetos sejam coletados como lixo. Todos os métodos estáticos e de instância em Microsoft.Extensions.ObjectPool são thread-safe.

Talvez os aplicativos queiram usar o pool de objetos se os objetos que estão sendo gerenciados forem:

  • Caro para alocar/inicializar.
  • Representa um recurso limitado.
  • Usado previsivelmente e com frequência.

Por exemplo, a estrutura ASP.NET Core usa o pool de objetos em alguns locais para reutilizar instâncias do StringBuilder. StringBuilder aloca e gerencia seus próprios buffers para armazenar dados de caracteres. O ASP.NET Core usa StringBuilder regularmente para implementar recursos e reutilizá-los oferece um benefício de desempenho.

O pool de objetos nem sempre melhora o desempenho:

  • A menos que o custo de inicialização de um objeto seja alto, geralmente é mais lento obter o objeto do pool.
  • Os objetos gerenciados pelo pool não são desalocados até que o pool seja desalocado.

Use o pool de objetos somente depois de coletar dados de desempenho usando cenários realistas para seu aplicativo ou biblioteca.

OBSERVAÇÃO: o ObjectPool não coloca um limite no número de objetos alocados, coloca um limite no número de objetos que ele retém.

Conceitos

Quando DefaultObjectPoolProvider é usado e T implementa IDisposable:

  • Os itens que não forem retornados ao pool serão descartados.
  • Quando o pool é descartado pela DI, todos os itens no pool são descartados.

OBSERVAÇÃO: depois que o pool é descartado:

  • Chamar Get gera um ObjectDisposedException.
  • Chamar Return descarta o item especificado.

Tipos e interfaces importantes do ObjectPool:

  • ObjectPool<T>: a abstração básica do pool de objetos. Usado para obter e retornar objetos.
  • PooledObjectPolicy<T>: implemente isso para personalizar como um objeto é criado e como ele é redefinido quando retornado ao pool. Isso pode ser passado para um pool de objetos que é construído diretamente ou
  • Create: atua como uma fábrica para criar pools de objetos.
  • IResettable: redefine automaticamente o objeto quando retornado para um pool de objetos.

O ObjectPool pode ser usado em um aplicativo de várias maneiras:

  • Instanciando um pool.
  • Registrar um pool na Injeção de dependência(DI) como uma instância.
  • Registrando o ObjectPoolProvider<> na DI e usando-o como uma fábrica.

Como usar ObjectPool

Chame Get para obter um objeto e Return para retornar o objeto . Não há nenhum requisito de que você retorne todos os objetos. Se você não retornar um objeto, ele será coletado.

Exemplo de ObjectPool

O seguinte código:

  • Adiciona ObjectPoolProvider ao contêiner de Injeção de dependência(DI).
  • Adiciona e configura ObjectPool<StringBuilder> ao contêiner de DI.
  • Adiciona o 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();

O código a seguir implementa 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 faz parte da infraestrutura do ASP.NET Core que dá suporte à manutenção de um grupo de objetos na memória para reutilização, em vez de permitir que os objetos sejam coletados como lixo.

Talvez você queira usar o pool de objetos se os objetos que estão sendo gerenciados forem:

  • Caro para alocar/inicializar.
  • Representa algum recurso limitado.
  • Usado previsivelmente e com frequência.

Por exemplo, a estrutura ASP.NET Core usa o pool de objetos em alguns locais para reutilizar instâncias do StringBuilder. StringBuilder aloca e gerencia seus próprios buffers para armazenar dados de caracteres. O ASP.NET Core usa StringBuilder regularmente para implementar recursos e reutilizá-los oferece um benefício de desempenho.

O pool de objetos nem sempre melhora o desempenho:

  • A menos que o custo de inicialização de um objeto seja alto, geralmente é mais lento obter o objeto do pool.
  • Os objetos gerenciados pelo pool não são desalocados até que o pool seja desalocado.

Use o pool de objetos somente depois de coletar dados de desempenho usando cenários realistas para seu aplicativo ou biblioteca.

AVISO: o ObjectPool não implementa IDisposable. Não recomendamos usá-lo com tipos que precisam de descarte. O ObjectPool no ASP.NET Core 3.0 e posteriores é compatível comIDisposable.

OBSERVAÇÃO: o ObjectPool não coloca um limite no número de objetos alocados, coloca um limite no número de objetos que ele retém.

Conceitos

ObjectPool<T>: a abstração básica do pool de objetos. Usado para obter e retornar objetos.

PooledObjectPolicy<T>: implemente isso para personalizar como um objeto é criado e como ele é redefinido quando retornado ao pool. Isso pode ser passado para um pool de objetos que você constrói diretamente...... OU

Create: atua como uma fábrica para criar pools de objetos.

O ObjectPool pode ser usado em um aplicativo de várias maneiras:

  • Instanciando um pool.
  • Registrar um pool na Injeção de dependência(DI) como uma instância.
  • Registrando o ObjectPoolProvider<> na DI e usando-o como uma fábrica.

Como usar ObjectPool

Chame Get para obter um objeto e Return para retornar o objeto . Não há nenhum requisito de que você retorne todos os objetos. Se você não retornar um objeto, ele será coletado.

Exemplo de ObjectPool

O seguinte código:

  • Adiciona ObjectPoolProvider ao contêiner de Injeção de dependência(DI).
  • Adiciona e configura ObjectPool<StringBuilder> ao contêiner de DI.
  • Adiciona o 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>(); 
    }
}

O código a seguir implementa 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);
    }
}