Riutilizzo di oggetti con ObjectPool in ASP.NET Core

Di Günther Foidl, Steve Gordon e Samson Amaugo

Nota

Questa non è la versione più recente di questo articolo. Per la versione corrente, vedere la versione .NET 8 di questo articolo.

Avviso

Questa versione di ASP.NET Core non è più supportata. Per altre informazioni, vedere Criteri di supporto di .NET e .NET Core. Per la versione corrente, vedere la versione .NET 8 di questo articolo.

Importante

Queste informazioni si riferiscono a un prodotto non definitive che può essere modificato in modo sostanziale prima che venga rilasciato commercialmente. Microsoft non riconosce alcuna garanzia, espressa o implicita, in merito alle informazioni qui fornite.

Per la versione corrente, vedere la versione .NET 8 di questo articolo.

Microsoft.Extensions.ObjectPool fa parte dell'infrastruttura ASP.NET Core che supporta il mantenimento di un gruppo di oggetti in memoria per il riutilizzo anziché consentire il Garbage Collection degli oggetti. Tutti i metodi statici e di istanza in Microsoft.Extensions.ObjectPool sono thread-safe.

Le app potrebbero voler usare il pool di oggetti se gli oggetti gestiti sono:

  • Costoso da allocare/inizializzare.
  • Rappresenta una risorsa limitata.
  • Usato in modo prevedibile e frequente.

Ad esempio, il framework ASP.NET Core usa il pool di oggetti in alcune posizioni per riutilizzare StringBuilder le istanze. StringBuilder alloca e gestisce i propri buffer per contenere i dati di tipo carattere. ASP.NET Core usa StringBuilder regolarmente per implementare le funzionalità e riutilizzarle offre un vantaggio per le prestazioni.

Il pool di oggetti non migliora sempre le prestazioni:

  • A meno che il costo di inizializzazione di un oggetto non sia elevato, in genere è più lento ottenere l'oggetto dal pool.
  • Gli oggetti gestiti dal pool non vengono deallocati fino a quando il pool non viene deallocato.

Usare il pool di oggetti solo dopo la raccolta di dati sulle prestazioni usando scenari realistici per l'app o la libreria.

NOTA: ObjectPool non inserisce un limite al numero di oggetti allocati, ma limita il numero di oggetti che mantiene.

Concetti relativi a ObjectPool

Quando DefaultObjectPoolProvider viene usato e T implementato IDisposable:

  • Gli elementi non restituiti al pool verranno eliminati.
  • Quando il pool viene eliminato dall'inserimento delle dipendenze, tutti gli elementi nel pool vengono eliminati.

NOTA: dopo l'eliminazione del pool:

  • La chiamata Get genera un'eccezione ObjectDisposedException.
  • La chiamata Return elimina l'elemento specificato.

Tipi e interfacce importanti ObjectPool :

  • ObjectPool<T> : astrazione del pool di oggetti di base. Utilizzato per ottenere e restituire oggetti.
  • PooledObjectPolicy<T> : implementare questa opzione per personalizzare il modo in cui viene creato un oggetto e come viene reimpostato quando viene restituito al pool. Può essere passato a un pool di oggetti costruito direttamente.
  • IResettable : reimposta automaticamente l'oggetto quando viene restituito a un pool di oggetti.

ObjectPool può essere usato in un'app in diversi modi:

  • Creazione di un'istanza di un pool.
  • Registrazione di un pool in inserimento delle dipendenze (DI) come istanza.
  • Registrazione dell'oggetto ObjectPoolProvider<> in inserimento di dipendenze e uso come factory.

Come usare ObjectPool

Chiamare Get per ottenere un oggetto e Return per restituire l'oggetto . Non è necessario restituire ogni oggetto. Se un oggetto non viene restituito, verrà sottoposto a Garbage Collection.

Esempio di ObjectPool

Il codice seguente:

  • Aggiunge ObjectPoolProvider al contenitore dependency injection (DI).
  • Implementa l'interfaccia IResettable per cancellare automaticamente il contenuto del buffer quando viene restituito al pool di oggetti.
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;
    }
}

NOTA: quando il tipo T in pool non implementa IResettable, è possibile usare un oggetto personalizzato PooledObjectPolicy<T> per reimpostare lo stato degli oggetti prima che vengano restituiti al pool.

Microsoft.Extensions.ObjectPool fa parte dell'infrastruttura ASP.NET Core che supporta il mantenimento di un gruppo di oggetti in memoria per il riutilizzo anziché consentire il Garbage Collection degli oggetti. Tutti i metodi statici e di istanza in Microsoft.Extensions.ObjectPool sono thread-safe.

Le app potrebbero voler usare il pool di oggetti se gli oggetti gestiti sono:

  • Costoso da allocare/inizializzare.
  • Rappresenta una risorsa limitata.
  • Usato in modo prevedibile e frequente.

Ad esempio, il framework ASP.NET Core usa il pool di oggetti in alcune posizioni per riutilizzare StringBuilder le istanze. StringBuilder alloca e gestisce i propri buffer per contenere i dati di tipo carattere. ASP.NET Core usa StringBuilder regolarmente per implementare le funzionalità e riutilizzarle offre un vantaggio per le prestazioni.

Il pool di oggetti non migliora sempre le prestazioni:

  • A meno che il costo di inizializzazione di un oggetto non sia elevato, in genere è più lento ottenere l'oggetto dal pool.
  • Gli oggetti gestiti dal pool non vengono deallocati fino a quando il pool non viene deallocato.

Usare il pool di oggetti solo dopo la raccolta di dati sulle prestazioni usando scenari realistici per l'app o la libreria.

NOTA: ObjectPool non inserisce un limite al numero di oggetti allocati, ma limita il numero di oggetti che mantiene.

Concetti

Quando DefaultObjectPoolProvider viene usato e T implementato IDisposable:

  • Gli elementi non restituiti al pool verranno eliminati.
  • Quando il pool viene eliminato dall'inserimento delle dipendenze, tutti gli elementi nel pool vengono eliminati.

NOTA: dopo l'eliminazione del pool:

  • La chiamata Get genera un'eccezione ObjectDisposedException.
  • La chiamata Return elimina l'elemento specificato.

Tipi e interfacce importanti ObjectPool :

  • ObjectPool<T> : astrazione del pool di oggetti di base. Utilizzato per ottenere e restituire oggetti.
  • PooledObjectPolicy<T> : implementare questa opzione per personalizzare il modo in cui viene creato un oggetto e come viene reimpostato quando viene restituito al pool. Questa operazione può essere passata in un pool di oggetti che è costruito direttamente o
  • Create : funge da factory per la creazione di pool di oggetti.
  • IResettable : reimposta automaticamente l'oggetto quando viene restituito a un pool di oggetti.

ObjectPool può essere usato in un'app in diversi modi:

  • Creazione di un'istanza di un pool.
  • Registrazione di un pool in inserimento delle dipendenze (DI) come istanza.
  • Registrazione dell'oggetto ObjectPoolProvider<> in inserimento di dipendenze e uso come factory.

Come usare ObjectPool

Chiamare Get per ottenere un oggetto e Return per restituire l'oggetto . Non è necessario che vengano restituiti tutti gli oggetti. Se non si restituisce un oggetto, verrà sottoposto a Garbage Collection.

Esempio di ObjectPool

Il codice seguente:

  • Aggiunge ObjectPoolProvider al contenitore dependency injection (DI).
  • Aggiunge e configura al contenitore di inserimento delle dipendenze ObjectPool<StringBuilder> .
  • Aggiunge l'oggetto 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();

Il codice seguente 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 fa parte dell'infrastruttura ASP.NET Core che supporta il mantenimento di un gruppo di oggetti in memoria per il riutilizzo anziché consentire il Garbage Collection degli oggetti.

È possibile usare il pool di oggetti se gli oggetti gestiti sono:

  • Costoso da allocare/inizializzare.
  • Rappresenta una risorsa limitata.
  • Usato in modo prevedibile e frequente.

Ad esempio, il framework ASP.NET Core usa il pool di oggetti in alcune posizioni per riutilizzare StringBuilder le istanze. StringBuilder alloca e gestisce i propri buffer per contenere i dati di tipo carattere. ASP.NET Core usa StringBuilder regolarmente per implementare le funzionalità e riutilizzarle offre un vantaggio per le prestazioni.

Il pool di oggetti non migliora sempre le prestazioni:

  • A meno che il costo di inizializzazione di un oggetto non sia elevato, in genere è più lento ottenere l'oggetto dal pool.
  • Gli oggetti gestiti dal pool non vengono deallocati fino a quando il pool non viene deallocato.

Usare il pool di oggetti solo dopo la raccolta di dati sulle prestazioni usando scenari realistici per l'app o la libreria.

AVVISO: ObjectPool non implementa IDisposable. Non è consigliabile usarlo con i tipi che richiedono lo smaltimento.ObjectPool in ASP.NET Core 3.0 e versioni successive supporta IDisposable.

NOTA: ObjectPool non inserisce un limite al numero di oggetti che alloca, ma limita il numero di oggetti che manterrà.

Concetti

ObjectPool<T> : astrazione del pool di oggetti di base. Utilizzato per ottenere e restituire oggetti.

PooledObjectPolicy<T> : implementare questa opzione per personalizzare la modalità di creazione di un oggetto e la modalità di reimpostazione quando viene restituito al pool. Questa operazione può essere passata in un pool di oggetti che si costruisce direttamente. O

Create funge da factory per la creazione di pool di oggetti.

ObjectPool può essere usato in un'app in diversi modi:

  • Creazione di un'istanza di un pool.
  • Registrazione di un pool in inserimento delle dipendenze (DI) come istanza.
  • Registrazione dell'oggetto ObjectPoolProvider<> in inserimento di dipendenze e uso come factory.

Come usare ObjectPool

Chiamare Get per ottenere un oggetto e Return per restituire l'oggetto . Non è necessario che vengano restituiti tutti gli oggetti. Se non si restituisce un oggetto, verrà sottoposto a Garbage Collection.

Esempio di ObjectPool

Il codice seguente:

  • Aggiunge ObjectPoolProvider al contenitore dependency injection (DI).
  • Aggiunge e configura al contenitore di inserimento delle dipendenze ObjectPool<StringBuilder> .
  • Aggiunge l'oggetto 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>(); 
    }
}

Il codice seguente 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);
    }
}