Dela via


> Riktlinjer för T- och Span<T-användning> för minne<

.NET innehåller ett antal typer som representerar en godtycklig sammanhängande minnesregion. Span<T> och ReadOnlySpan<T> är lätta minnesbuffertar som omsluter referenser till hanterat eller ohanterat minne. Eftersom dessa typer bara kan lagras på stacken är de olämpliga för scenarier som asynkrona metodanrop. För att lösa det här problemet har .NET 2.1 lagt till några ytterligare typer, till exempel Memory<T>, ReadOnlyMemory<T>, IMemoryOwner<T>och MemoryPool<T>. Som Span<T>, Memory<T> och dess relaterade typer kan backas upp av både hanterat och ohanterat minne. Till skillnad från Span<T>kan Memory<T> lagras på den hanterade heapen.

Både Span<T> och Memory<T> är omslutningar över buffertar med strukturerade data som kan användas i pipelines. De är alltså utformade så att vissa eller alla data effektivt kan skickas till komponenter i pipelinen, vilket kan bearbeta dem och eventuellt ändra bufferten. Eftersom Memory<T> och dess relaterade typer kan nås av flera komponenter eller av flera trådar är det viktigt att följa vissa riktlinjer för standardanvändning för att skapa robust kod.

Ägare, konsumenter och livslängdshantering

Buffertar kan skickas mellan API:er och kan ibland nås från flera trådar, så var medveten om hur en bufferts livslängd hanteras. Det finns tre grundläggande begrepp:

  • Ägarskap. Ägaren till en buffertinstans ansvarar för livstidshantering, inklusive att förstöra bufferten när den inte längre används. Alla buffertar har en enda ägare. Vanligtvis är ägaren den komponent som skapade bufferten eller som tog emot bufferten från en fabrik. Ägarskap kan också överföras; Komponent-A kan avstå från kontrollen över bufferten till Komponent-B, då Komponent-A kanske inte längre använder bufferten, och Komponent-B blir ansvarig för att förstöra bufferten när den inte längre används.

  • Förbrukning. Konsumenten av en buffertinstans får använda buffertinstansen genom att läsa från den och eventuellt skriva till den. Buffertar kan ha en konsument i taget om inte någon extern synkroniseringsmekanism tillhandahålls. Den aktiva konsumenten av en buffert är inte nödvändigtvis buffertens ägare.

  • Lån. Lånet är den tid som en viss komponent tillåts vara buffertens konsument.

Följande pseudokodexempel illustrerar dessa tre begrepp. Buffer i pseudokoden representerar en Memory<T> eller Span<T> en buffert av typen Char. Metoden Main instansierar bufferten WriteInt32ToBuffer , anropar metoden för att skriva strängrepresentationen av ett heltal till bufferten och anropar DisplayBufferToConsole sedan metoden för att visa buffertens värde.

using System;

class Program
{
    // Write 'value' as a human-readable string to the output buffer.
    void WriteInt32ToBuffer(int value, Buffer buffer);

    // Display the contents of the buffer to the console.
    void DisplayBufferToConsole(Buffer buffer);

    // Application code
    static void Main()
    {
        var buffer = CreateBuffer();
        try
        {
            int value = Int32.Parse(Console.ReadLine());
            WriteInt32ToBuffer(value, buffer);
            DisplayBufferToConsole(buffer);
        }
        finally
        {
            buffer.Destroy();
        }
    }
}

Metoden Main skapar bufferten och dess ägare också. Main Därför ansvarar du för att förstöra bufferten när den inte längre används. Pseudokoden illustrerar detta genom att anropa en Destroy metod i bufferten. (Varken Memory<T> eller Span<T> faktiskt har en Destroy metod. Du ser faktiska kodexempel senare i den här artikeln.)

Bufferten har två konsumenter och WriteInt32ToBufferDisplayBufferToConsole. Det finns bara en konsument i taget (först WriteInt32ToBuffer, sedan DisplayBufferToConsole) och ingen av konsumenterna äger bufferten. Observera också att "konsument" i det här sammanhanget inte innebär en skrivskyddad vy av bufferten. konsumenter kan ändra buffertens innehåll, precis som WriteInt32ToBuffer om de får en läs-/skrivvy av bufferten.

Metoden WriteInt32ToBuffer har ett lån på (kan förbruka) bufferten mellan början av metodanropet och den tid då metoden returneras. På samma sätt har du ett lån på bufferten medan den körs och lånet DisplayBufferToConsole frigörs när metoden varvar ned. (Det finns inget API för leasinghantering, ett "lån" är en begreppsfråga.)

Minne<T> och ägar-/konsumentmodellen

Som avsnittet Ägare, konsumenter och livslängdshantering konstaterar har en buffert alltid en ägare. .NET har stöd för två ägarskapsmodeller:

  • En modell som stöder enskilt ägande. En buffert har en enskild ägare under hela livslängden.

  • En modell som stöder ägarskapsöverföring. Ägarskapet för en buffert kan överföras från dess ursprungliga ägare (dess skapare) till en annan komponent, som sedan blir ansvarig för buffertens livslängdshantering. Ägaren kan i sin tur överföra ägarskapet till en annan komponent och så vidare.

Du använder System.Buffers.IMemoryOwner<T> gränssnittet för att uttryckligen hantera ägarskapet för en buffert. IMemoryOwner<T> stöder båda ägarskapsmodellerna. Komponenten som har en IMemoryOwner<T> referens äger bufferten. I följande exempel används en IMemoryOwner<T> instans för att återspegla ägarskapet för en Memory<T> buffert.

using System;
using System.Buffers;

class Example
{
    static void Main()
    {
        IMemoryOwner<char> owner = MemoryPool<char>.Shared.Rent();

        Console.Write("Enter a number: ");
        try
        {
            string? s = Console.ReadLine();

            if (s is null)
                return;

            var value = Int32.Parse(s);

            var memory = owner.Memory;

            WriteInt32ToBuffer(value, memory);

            DisplayBufferToConsole(owner.Memory.Slice(0, value.ToString().Length));
        }
        catch (FormatException)
        {
            Console.WriteLine("You did not enter a valid number.");
        }
        catch (OverflowException)
        {
            Console.WriteLine($"You entered a number less than {Int32.MinValue:N0} or greater than {Int32.MaxValue:N0}.");
        }
        finally
        {
            owner?.Dispose();
        }
    }

    static void WriteInt32ToBuffer(int value, Memory<char> buffer)
    {
        var strValue = value.ToString();

        var span = buffer.Span;
        for (int ctr = 0; ctr < strValue.Length; ctr++)
            span[ctr] = strValue[ctr];
    }

    static void DisplayBufferToConsole(Memory<char> buffer) =>
        Console.WriteLine($"Contents of the buffer: '{buffer}'");
}

Vi kan också skriva det här exemplet med -instruktionenusing:

using System;
using System.Buffers;

class Example
{
    static void Main()
    {
        using (IMemoryOwner<char> owner = MemoryPool<char>.Shared.Rent())
        {
            Console.Write("Enter a number: ");
            try
            {
                string? s = Console.ReadLine();

                if (s is null)
                    return;

                var value = Int32.Parse(s);

                var memory = owner.Memory;
                WriteInt32ToBuffer(value, memory);
                DisplayBufferToConsole(memory.Slice(0, value.ToString().Length));
            }
            catch (FormatException)
            {
                Console.WriteLine("You did not enter a valid number.");
            }
            catch (OverflowException)
            {
                Console.WriteLine($"You entered a number less than {Int32.MinValue:N0} or greater than {Int32.MaxValue:N0}.");
            }
        }
    }

    static void WriteInt32ToBuffer(int value, Memory<char> buffer)
    {
        var strValue = value.ToString();

        var span = buffer.Slice(0, strValue.Length).Span;
        strValue.AsSpan().CopyTo(span);
    }

    static void DisplayBufferToConsole(Memory<char> buffer) =>
        Console.WriteLine($"Contents of the buffer: '{buffer}'");
}

I den här koden:

  • Metoden Main innehåller referensen till instansen IMemoryOwner<T>Main , så metoden är buffertens ägare.

  • Metoderna WriteInt32ToBuffer och DisplayBufferToConsole accepterar Memory<T> som ett offentligt API. Därför är de konsumenter av bufferten. Dessa metoder använder bufferten en i taget.

WriteInt32ToBuffer Metoden är avsedd att skriva ett värde till buffertenDisplayBufferToConsole, men metoden är inte avsedd att göra det. För att återspegla detta kunde det ha accepterat ett argument av typen ReadOnlyMemory<T>. Mer information om finns i Regel 2: Använd ReadOnlySpan<T> eller ReadOnlyMemory<T> om bufferten ska vara skrivskyddad.ReadOnlyMemory<T>

"Ägarlösa" minnes-T-instanser<>

Du kan skapa en Memory<T> instans utan att använda IMemoryOwner<T>. I det här fallet är ägarskapet för bufferten implicit snarare än explicit, och endast modellen med en ägare stöds. Du kan göra detta på följande sätt:

using System;

class Example
{
    static void Main()
    {
        Memory<char> memory = new char[64];

        Console.Write("Enter a number: ");
        string? s = Console.ReadLine();

        if (s is null)
            return;

        var value = Int32.Parse(s);

        WriteInt32ToBuffer(value, memory);
        DisplayBufferToConsole(memory);
    }

    static void WriteInt32ToBuffer(int value, Memory<char> buffer)
    {
        var strValue = value.ToString();
        strValue.AsSpan().CopyTo(buffer.Slice(0, strValue.Length).Span);
    }

    static void DisplayBufferToConsole(Memory<char> buffer) =>
        Console.WriteLine($"Contents of the buffer: '{buffer}'");
}

Den metod som ursprungligen skapar instansen Memory<T> är buffertens implicita ägare. Ägarskap kan inte överföras till någon annan komponent eftersom det inte finns någon IMemoryOwner<T> instans som underlättar överföringen. (Som ett alternativ kan du också tänka dig att körningens skräpinsamlare äger bufferten och att alla metoder bara använder bufferten.)

Riktlinjer för användning

Eftersom ett minnesblock ägs men är avsett att skickas till flera komponenter, varav vissa kan användas på ett visst minnesblock samtidigt, är det viktigt att upprätta riktlinjer för att använda både Memory<T> och Span<T>. Riktlinjer är nödvändiga eftersom det är möjligt för en komponent att:

  • Behåll en referens till ett minnesblock när dess ägare har släppt det.

  • Arbeta på en buffert samtidigt som en annan komponent körs på den, samtidigt som data i bufferten skadas.

  • Även om den stackallokerade typen av optimerar prestanda och gör Span<T> den föredragna typen för att arbeta på ett minnesblock, omfattas Span<T> den också av Span<T> några större begränsningar. Det är viktigt att veta när du ska använda en Span<T> och när du ska använda Memory<T>.

Följande är våra rekommendationer för att använda Memory<T> och dess relaterade typer. Vägledning som gäller för Memory<T> och Span<T> även gäller för ReadOnlyMemory<T> och ReadOnlySpan<T> om inget annat anges.

Regel nr 1: För ett synkront API använder du Span<T> i stället för Minne<T> som en parameter om möjligt.

Span<T> är mer mångsidig än Memory<T> och kan representera en bredare mängd sammanhängande minnesbuffertar. Span<T> ger också bättre prestanda än Memory<T>. Slutligen kan du använda Memory<T>.Span egenskapen för att konvertera en Memory<T> instans till en Span<T>, även om T-till-minne<<> T-konvertering> inte är möjligt. Så om dina uppringare råkar ha en Memory<T> instans kan de anropa dina metoder med Span<T> parametrar ändå.

Genom att använda en parameter av typen Span<T> i stället för typ Memory<T> kan du också skriva en korrekt metodimplementering. Du får automatiskt kompileringstidskontroller för att säkerställa att du inte försöker komma åt bufferten utöver metodens lån (mer om detta senare).

Ibland måste du använda en Memory<T> parameter i stället för en Span<T> parameter, även om du är helt synkron. Kanske accepterar ett API som du är beroende av endast Memory<T> argument. Detta är bra, men var medveten om de kompromisser som är inblandade när du använder Memory<T> synkront.

Regel nr 2: Använd ReadOnlySpan<T> eller ReadOnlyMemory<T> om bufferten ska vara skrivskyddad.

I de tidigare exemplen DisplayBufferToConsole läser metoden bara från bufferten. Den ändrar inte innehållet i bufferten. Metodsignaturen bör ändras till följande.

void DisplayBufferToConsole(ReadOnlyMemory<char> buffer);

Om vi kombinerar den här regeln och regel 1 kan vi faktiskt göra ännu bättre och skriva om metodsignaturen på följande sätt:

void DisplayBufferToConsole(ReadOnlySpan<char> buffer);

Metoden DisplayBufferToConsole fungerar nu med praktiskt taget alla tänkbara bufferttyper: T[], lagring som allokerats med stackalloc och så vidare. Du kan till och med skicka en String direkt in i den! Mer information finns i GitHub-problem med dotnet/docs #25551.

Regel nr 3: Om metoden accepterar Minne<T> och returnerar voidfår du inte använda T-instansen för minne<> när metoden har returnerats.

Detta gäller begreppet "lån" som nämndes tidigare. En void-returning-metod lån på instansen Memory<T> börjar när metoden anges och slutar när metoden avslutas. Tänk på följande exempel, som anropar Log i en loop baserat på indata från konsolen.

using System;
using System.Buffers;

public class Example
{
    // implementation provided by third party
    static extern void Log(ReadOnlyMemory<char> message);

    // user code
    public static void Main()
    {
        using (var owner = MemoryPool<char>.Shared.Rent())
        {
            var memory = owner.Memory;
            var span = memory.Span;
            while (true)
            {
                string? s = Console.ReadLine();

                if (s is null)
                    return;

                int value = Int32.Parse(s);
                if (value < 0)
                    return;

                int numCharsWritten = ToBuffer(value, span);
                Log(memory.Slice(0, numCharsWritten));
            }
        }
    }

    private static int ToBuffer(int value, Span<char> span)
    {
        string strValue = value.ToString();
        int length = strValue.Length;
        strValue.AsSpan().CopyTo(span.Slice(0, length));
        return length;
    }
}

Om Log är en helt synkron metod fungerar den här koden som förväntat eftersom det bara finns en aktiv konsument av minnesinstansen vid en viss tidpunkt. Men föreställ dig i stället att det Log har denna implementering.

// !!! INCORRECT IMPLEMENTATION !!!
static void Log(ReadOnlyMemory<char> message)
{
    // Run in background so that we don't block the main thread while performing IO.
    Task.Run(() =>
    {
        StreamWriter sw = File.AppendText(@".\input-numbers.dat");
        sw.WriteLine(message);
    });
}

I den här implementeringen Log bryter mot lånet eftersom den fortfarande försöker använda instansen Memory<T> i bakgrunden efter att den ursprungliga metoden har returnerats. Metoden Main kan mutera bufferten vid Log försök att läsa från den, vilket kan leda till att data skadas.

Det finns flera sätt att lösa detta:

  • Metoden Log kan returnera en Task i stället för void, som följande implementering av Log metoden gör.

    // An acceptable implementation.
    static Task Log(ReadOnlyMemory<char> message)
    {
        // Run in the background so that we don't block the main thread while performing IO.
        return Task.Run(() => {
            StreamWriter sw = File.AppendText(@".\input-numbers.dat");
            sw.WriteLine(message);
            sw.Flush();
        });
    }
    
  • Log kan i stället implementeras på följande sätt:

    // An acceptable implementation.
    static void Log(ReadOnlyMemory<char> message)
    {
        string defensiveCopy = message.ToString();
        // Run in the background so that we don't block the main thread while performing IO.
        Task.Run(() =>
        {
            StreamWriter sw = File.AppendText(@".\input-numbers.dat");
            sw.WriteLine(defensiveCopy);
            sw.Flush();
        });
    }
    

Regel 4: Om din metod accepterar ett minne<T> och returnerar en aktivitet får du inte använda T-instansen för minne<> efter att aktiviteten har övergått till ett terminaltillstånd.

Det här är bara asynkron variant av regel 3. Metoden Log från det tidigare exemplet kan skrivas enligt följande för att följa den här regeln:

// An acceptable implementation.
static Task Log(ReadOnlyMemory<char> message)
{
    // Run in the background so that we don't block the main thread while performing IO.
    return Task.Run(() =>
    {
        StreamWriter sw = File.AppendText(@".\input-numbers.dat");
        sw.WriteLine(message);
        sw.Flush();
    });
}

Här innebär "terminaltillstånd" att uppgiften övergår till ett slutfört, felat eller avbrutet tillstånd. Med andra ord betyder "terminaltillstånd" "allt som skulle orsaka väntar på att utlösa eller fortsätta körningen".

Den här vägledningen gäller för metoder som returnerar Task, Task<TResult>, ValueTask<TResult>eller någon liknande typ.

Regel nr 5: Om konstruktorn accepterar T-minne>< som parameter antas instansmetoder på det konstruerade objektet vara konsumenter av T-instansen för minne><.

Ta följande som exempel:

class OddValueExtractor
{
    public OddValueExtractor(ReadOnlyMemory<int> input);
    public bool TryReadNextOddValue(out int value);
}

void PrintAllOddValues(ReadOnlyMemory<int> input)
{
    var extractor = new OddValueExtractor(input);
    while (extractor.TryReadNextOddValue(out int value))
    {
      Console.WriteLine(value);
    }
}

OddValueExtractor Här accepterar konstruktorn en ReadOnlyMemory<int> som konstruktorparameter, så konstruktorn själv är en konsument av instansenReadOnlyMemory<int>, och alla instansmetoder på det returnerade värdet är också konsumenter av den ursprungliga ReadOnlyMemory<int> instansen. Det innebär att TryReadNextOddValue använder instansen ReadOnlyMemory<int> , även om instansen inte skickas direkt till TryReadNextOddValue metoden.

Regel nr 6: Om du har en t-typad<> egenskap (eller en motsvarande instansmetod) för din typ antas instansmetoder för objektet vara konsumenter av T-instansen minne<>.

Det här är egentligen bara en variant av regel 5. Den här regeln finns eftersom egenskapsuppsättningar eller motsvarande metoder antas samla in och spara indata, så instansmetoder på samma objekt kan använda det insamlade tillståndet.

Följande exempel utlöser den här regeln:

class Person
{
    // Settable property.
    public Memory<char> FirstName { get; set; }

    // alternatively, equivalent "setter" method
    public SetFirstName(Memory<char> value);

    // alternatively, a public settable field
    public Memory<char> FirstName;
}

Regel 7: Om du har en IMemoryOwner<T-referens> måste du någon gång göra dig av med den eller överföra ägarskapet (men inte båda).

Eftersom en Memory<T> instans kan backas upp av antingen hanterat eller ohanterat minne måste ägaren anropa DisposeIMemoryOwner<T> när arbetet som utförs på instansen Memory<T> är klart. Alternativt kan ägaren överföra ägarskapet för instansen IMemoryOwner<T> till en annan komponent, då den förvärvande komponenten blir ansvarig för att anropa Dispose vid lämplig tidpunkt (mer om detta senare).

Om metoden inte anropas Dispose på en IMemoryOwner<T> instans kan det leda till ohanterade minnesläckor eller annan prestandaförsämring.

Den här regeln gäller även för kod som anropar fabriksmetoder som MemoryPool<T>.Rent. Anroparen blir ägare till den returnerade IMemoryOwner<T> och ansvarar för att ta bort instansen när den är klar.

Regel 8: Om du har en IMemoryOwner<T-parameter> på API-ytan godkänner du ägarskapet för den instansen.

Om du accepterar en instans av den här typen signaleras att komponenten har för avsikt att ta över ägarskapet för den här instansen. Komponenten blir ansvarig för korrekt bortskaffande enligt regel 7.

Alla komponenter som överför ägarskapet för instansen IMemoryOwner<T> till en annan komponent bör inte längre använda den instansen när metodanropet har slutförts.

Viktigt!

Om konstruktorn accepterar IMemoryOwner<T> som en parameter bör dess typ implementera IDisposableoch metoden Dispose ska anropa Dispose objektet IMemoryOwner<T> .

Regel nr 9: Om du omsluter en synkron p/invoke-metod bör DITT API acceptera Span<T> som en parameter.

Enligt regel nr 1 Span<T> är vanligtvis rätt typ att använda för synkrona API:er. Du kan fästa Span<T> instanser via nyckelordet fixed , som i följande exempel.

using System.Runtime.InteropServices;

[DllImport(...)]
private static extern unsafe int ExportedMethod(byte* pbData, int cbData);

public unsafe int ManagedWrapper(Span<byte> data)
{
    fixed (byte* pbData = &MemoryMarshal.GetReference(data))
    {
        int retVal = ExportedMethod(pbData, data.Length);

        /* error checking retVal goes here */

        return retVal;
    }
}

I föregående exempel pbData kan vara null om till exempel indataintervallet är tomt. Om den exporterade metoden absolut kräver att pbData den inte är null, även om cbData den är 0, kan metoden implementeras på följande sätt:

public unsafe int ManagedWrapper(Span<byte> data)
{
    fixed (byte* pbData = &MemoryMarshal.GetReference(data))
    {
        byte dummy = 0;
        int retVal = ExportedMethod((pbData != null) ? pbData : &dummy, data.Length);

        /* error checking retVal goes here */

        return retVal;
    }
}

Regel nr 10: Om du omsluter en asynkron p/invoke-metod bör API:et acceptera Memory<T> som en parameter.

Eftersom du inte kan använda nyckelordet fixed för asynkrona åtgärder använder Memory<T>.Pin du metoden för att fästa Memory<T> instanser, oavsett vilken typ av sammanhängande minne som instansen representerar. I följande exempel visas hur du använder det här API:et för att utföra ett asynkront p/invoke-anrop.

using System.Runtime.InteropServices;

[UnmanagedFunctionPointer(...)]
private delegate void OnCompletedCallback(IntPtr state, int result);

[DllImport(...)]
private static extern unsafe int ExportedAsyncMethod(byte* pbData, int cbData, IntPtr pState, IntPtr lpfnOnCompletedCallback);

private static readonly IntPtr _callbackPtr = GetCompletionCallbackPointer();

public unsafe Task<int> ManagedWrapperAsync(Memory<byte> data)
{
    // setup
    var tcs = new TaskCompletionSource<int>();
    var state = new MyCompletedCallbackState
    {
        Tcs = tcs
    };
    var pState = (IntPtr)GCHandle.Alloc(state);

    var memoryHandle = data.Pin();
    state.MemoryHandle = memoryHandle;

    // make the call
    int result;
    try
    {
        result = ExportedAsyncMethod((byte*)memoryHandle.Pointer, data.Length, pState, _callbackPtr);
    }
    catch
    {
        ((GCHandle)pState).Free(); // cleanup since callback won't be invoked
        memoryHandle.Dispose();
        throw;
    }

    if (result != PENDING)
    {
        // Operation completed synchronously; invoke callback manually
        // for result processing and cleanup.
        MyCompletedCallbackImplementation(pState, result);
    }

    return tcs.Task;
}

private static void MyCompletedCallbackImplementation(IntPtr state, int result)
{
    GCHandle handle = (GCHandle)state;
    var actualState = (MyCompletedCallbackState)(handle.Target);
    handle.Free();
    actualState.MemoryHandle.Dispose();

    /* error checking result goes here */

    if (error)
    {
        actualState.Tcs.SetException(...);
    }
    else
    {
        actualState.Tcs.SetResult(result);
    }
}

private static IntPtr GetCompletionCallbackPointer()
{
    OnCompletedCallback callback = MyCompletedCallbackImplementation;
    GCHandle.Alloc(callback); // keep alive for lifetime of application
    return Marshal.GetFunctionPointerForDelegate(callback);
}

private class MyCompletedCallbackState
{
    public TaskCompletionSource<int> Tcs;
    public MemoryHandle MemoryHandle;
}

Se även