Freigeben über


Leitfaden zur Verwendung von Memory<T> und Span<T>

.NET enthält eine Reihe von Typen, die einen beliebigen zusammenhängenden Speicherbereich darstellen. Span<T> und ReadOnlySpan<T> sind einfache Speicherpuffer, die Verweise auf verwalteten oder nicht verwalteten Speicher umschließen. Da diese Typen nur im Stapel gespeichert werden können, sind sie für Szenarien wie asynchrone Methodenaufrufe nicht geeignet. Um dieses Problem zu beheben, hat .NET 2.1 einige zusätzliche Typen hinzugefügt, darunter Memory<T>, , ReadOnlyMemory<T>, IMemoryOwner<T>und MemoryPool<T>. Wie Span<T> können Memory<T> und die zugehörigen Typen sowohl auf verwalteten als auch auf nicht verwalteten Speicher zurückgreifen. Im Gegensatz zu Span<T> kann Memory<T> auf dem verwalteten Heap gespeichert werden.

Sowohl Span<T> als auch Memory<T> sind Wrapper über Puffer von strukturierten Daten, die in Pipelines verwendet werden können. Das heißt, sie sind so konzipiert, dass einige oder alle Daten effizient an Komponenten in der Pipeline übergeben werden können, die sie verarbeiten und optional den Puffer ändern können. Da Memory<T> und die zugehörigen Typen von mehreren Komponenten oder von mehreren Threads aufgerufen werden können, ist es wichtig, einige Standardverwendungsrichtlinien zu befolgen, um robusten Code zu erstellen.

Besitzer, Verbraucher und Lebenslanges Management

Puffer können zwischen APIs übergeben werden und können manchmal über mehrere Threads aufgerufen werden. Beachten Sie daher, wie die Lebensdauer eines Puffers verwaltet wird. Es gibt drei Kernkonzepte:

  • Besitz. Der Besitzer einer Pufferinstanz ist für die Lebensdauerverwaltung verantwortlich, einschließlich des Löschens des Puffers, wenn er nicht mehr verwendet wird. Alle Puffer verfügen über einen einzelnen Besitzer. Im Allgemeinen ist der Besitzer die Komponente, die den Puffer erstellt oder den Puffer von einer Factory empfangen hat. Das Eigentum kann auch übertragen werden; Component-A kann die Kontrolle des Puffers an Component-B zuweisen, an welcher Stelle Component-A den Puffer möglicherweise nicht mehr verwendet, und Component-B wird dafür verantwortlich, den Puffer zu zerstören, wenn er nicht mehr verwendet wird.

  • Verbrauch. Der Verbraucher einer Pufferinstanz kann die Pufferinstanz verwenden, indem er aus ihr liest und möglicherweise in sie schreibt. Puffer können jeweils einen Verbraucher haben, es sei denn, es wird ein externer Synchronisierungsmechanismus bereitgestellt. Der aktive Verbraucher eines Puffers ist nicht unbedingt der Besitzer des Puffers.

  • Leasedauer. Die Leasedauer ist die Zeitspanne, in der eine bestimmte Komponente als Consumer des Puffers zugelassen wird.

Im folgenden Pseudocodebeispiel werden diese drei Konzepte veranschaulicht. Buffer im Pseudocode stellt entweder einen Memory<T>- oder Span<T>-Puffer vom Typ Char dar. Die Main Methode instanziiert den Puffer, ruft die WriteInt32ToBuffer Methode auf, um die Zeichenfolgendarstellung einer ganzen Zahl in den Puffer zu schreiben, und ruft dann die DisplayBufferToConsole Methode auf, um den Wert des Puffers anzuzeigen.

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

Die Main Methode erstellt den Puffer und ist daher dessen Eigentümer. Daher ist Main dafür verantwortlich, den Puffer zu zerstören, wenn er nicht mehr verwendet wird. Der Pseudocode veranschaulicht dies durch Aufrufen einer Destroy Methode für den Puffer. (Weder Memory<T> noch Span<T> hat eine Destroy-Methode. Die tatsächlichen Codebeispiele sind weiter unten in diesem Artikel zu sehen.)

Der Puffer hat zwei Verbraucher, WriteInt32ToBuffer und DisplayBufferToConsole. Es gibt jeweils nur einen Verbraucher (zuerst WriteInt32ToBuffer, dann DisplayBufferToConsole), und keiner der Verbraucher besitzt den Puffer. Beachten Sie auch, dass „Consumer“ in diesem Zusammenhang keine schreibgeschützte Ansicht des Puffers impliziert; Consumer können den Inhalt des Puffers ändern, wie WriteInt32ToBuffer, wenn sie eine Lese-/Schreibansicht des Puffers erhalten.

Die WriteInt32ToBuffer-Methode verfügt über eine Leasedauer (kann den Puffer zwischen dem Start des Methodenaufrufs und der Zeit, die die Methode zurückgibt, verbrauchen). Ebenso hat DisplayBufferToConsole eine Leasingdauer für den Puffer, während er ausgeführt wird, und die Leasedauer wird freigegeben, wenn die Methode entladen wird. (Es gibt keine API für die Leaseverwaltung; eine "Lease" ist eine konzeptionelle Angelegenheit.)

Memory<T> und das Besitzer/Consumer-Modell

Wie im Abschnitt "Besitzer, Verbraucher und Lebensdauerverwaltung" erläutert wird, hat ein Puffer immer einen Besitzer. .NET unterstützt zwei Besitzermodelle:

  • Ein Modell, das einen einzelnen Besitz unterstützt. Ein Puffer verfügt über einen einzelnen Besitzer für die gesamte Lebensdauer.
  • Ein Modell, das die Eigentumsübertragung unterstützt. Der Besitz eines Puffers kann von seinem ursprünglichen Besitzer (seinem Ersteller) auf eine andere Komponente übertragen werden, die dann für die Lebensdauerverwaltung des Puffers verantwortlich wird. Dieser Besitzer kann wiederum den Besitz an eine andere Komponente übertragen usw.

Sie verwenden die System.Buffers.IMemoryOwner<T> Schnittstelle, um den Besitz eines Puffers explizit zu verwalten. IMemoryOwner<T> unterstützt beide Besitzmodelle. Die Komponente, die über einen IMemoryOwner<T> Verweis verfügt, besitzt den Puffer. Im folgenden Beispiel wird eine IMemoryOwner<T> Instanz verwendet, um den Besitz eines Memory<T> Puffers widerzuspiegeln.

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

Sie können dieses Beispiel auch mit der using Anweisung schreiben:

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

In diesem Code:

  • Die Main Methode enthält den Verweis auf die IMemoryOwner<T> Instanz, sodass die Main Methode der Besitzer des Puffers ist.
  • Die WriteInt32ToBuffer und DisplayBufferToConsole Methoden akzeptieren beide Memory<T> als eine öffentliche API. Daher sind sie Verbraucher des Puffers. Diese Methoden verbrauchen den Puffer einzeln.

Obwohl die WriteInt32ToBuffer Methode zum Schreiben eines Werts in den Puffer vorgesehen ist, ist die DisplayBufferToConsole Methode nicht vorgesehen. Um dies widerzuspiegeln, könnte es ein Argument vom Typ ReadOnlyMemory<T>akzeptiert haben. Weitere Informationen ReadOnlyMemory<T>finden Sie unter Regel #2: Verwenden von ReadOnlySpan<T> oder ReadOnlyMemory<T> , wenn der Puffer schreibgeschützt sein soll.

Memory<T>-Instanzen ohne Besitzer

Sie können eine Memory<T>-Instanz erstellen, ohne IMemoryOwner<T> zu verwenden. In diesem Fall ist der Besitz des Puffers implizit und nicht explizit, und nur das Einzelbesitzermodell wird unterstützt. Dazu können Sie folgende Aktionen ausführen:

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

Die Methode, die zunächst die Memory<T> Instanz erstellt, ist der implizite Besitzer des Puffers. Der Besitz kann nicht an eine andere Komponente übertragen werden, da keine IMemoryOwner<T> Instanz zur Erleichterung der Übertragung vorhanden ist. (Alternativ ist es auch vorstellbar, dass der Garbage Collector der Runtime den Puffer besitzt und alle Methoden nur den Puffer verbrauchen.)

Nutzungsrichtlinien

Da ein Speicherblock im Besitz ist, aber an mehrere Komponenten übergeben werden soll, von denen einige möglicherweise gleichzeitig auf einen bestimmten Speicherblock zugreifen, ist es wichtig, Richtlinien für die Verwendung von sowohl Memory<T> als auch Span<T> festzulegen. Richtlinien sind erforderlich, da es möglich ist, dass eine Komponente:

  • Behalten eines Verweises auf einen Arbeitsspeicherblock, nachdem sein Besitzer ihn freigegeben hat
  • Arbeiten Sie gleichzeitig an einem Puffer, während eine andere Komponente gleichzeitig darauf zugreift, was zu einer Beschädigung der Daten im Puffer führt.

Während die stack-zugewiesene Natur von Span<T> die Leistung optimiert und Span<T> zum bevorzugten Typ für den Betrieb auf einem Speicherblock macht, unterliegt Span<T> auch einigen wichtigen Einschränkungen. Es ist wichtig zu wissen, wann man Span<T> einsetzen soll und wann Memory<T>.

Nachfolgend sind unsere Empfehlungen für die erfolgreiche Verwendung Memory<T> und die zugehörigen Typen aufgeführt. Anleitungen, die für Memory<T> und Span<T> gelten, gelten auch für ReadOnlyMemory<T> und ReadOnlySpan<T>, sofern nicht anders angegeben.

Regel Nr. 1: Verwenden Sie für eine synchrone API Span<T> anstelle von Memory<T> als Parameter, falls möglich.

Span<T> ist vielseitiger als Memory<T> und kann eine größere Vielfalt zusammenhängender Speicherpuffer darstellen. Span<T> bietet auch eine bessere Leistung als Memory<T>. Schließlich können Sie die Memory<T>.Span-Eigenschaft verwenden, um eine Memory<T>-Instanz in eine Span<T> zu konvertieren, obwohl die Span<T>-zu-Memory<T>-Konvertierung nicht möglich ist. Wenn Ihre Aufrufer also über eine Memory<T> Instanz verfügen, können sie Ihre Methoden trotzdem mit Span<T> Parametern aufrufen.

Die Verwendung eines Parameters vom Typ Span<T> statt des Typs Memory<T> hilft Ihnen auch dabei, eine korrekte Implementierung einer konsumierenden Methode zu schreiben. Sie erhalten automatisch Kompilierungszeitprüfungen, um sicherzustellen, dass Sie nicht versuchen, über die Lease Ihrer Methode hinaus auf den Puffer zuzugreifen (mehr dazu später).

Manchmal müssen Sie anstelle eines Memory<T> Parameters einen Span<T> Parameter verwenden, auch wenn Sie vollständig synchron sind. Vielleicht nimmt eine API, von der Sie abhängig sind, nur Memory<T> Argumente an. Dies ist in Ordnung, aber seien Sie sich der Kompromisse bewusst, die beim synchronen Verwenden Memory<T> entstehen.

Regel Nr. 2: Verwenden von ReadOnlySpan<T> oder ReadOnlyMemory<T> , wenn der Puffer schreibgeschützt sein soll

In den vorherigen Beispielen liest die DisplayBufferToConsole Methode nur aus dem Puffer; sie ändert nicht den Inhalt des Puffers. Die Methodensignatur sollte wie folgt geändert werden.

void DisplayBufferToConsole(ReadOnlyMemory<char> buffer);

Wenn Sie diese Regel und Regel #1 kombinieren, können wir die Methodensignatur sogar noch besser umschreiben:

void DisplayBufferToConsole(ReadOnlySpan<char> buffer);

Die DisplayBufferToConsole Methode funktioniert jetzt mit praktisch jedem Puffertyp, der vorstellbar ist: T[], Speicher, der mit Stackalloc zugeordnet ist usw. Sie können sogar ein String direkt hinein übergeben! Weitere Informationen finden Sie unter GitHub issue dotnet/docs #25551.

Regel Nr. 3: Wenn Ihre Methode Memory<T> akzeptiert und einen Wert vom Typ void zurückgibt, dürfen Sie die Instanz von Memory<T> nicht mehr verwenden, nachdem die Methode beendet wurde.

Dies bezieht sich auf das zuvor erwähnte "Lease"-Konzept. Die Leasedauer für eine Methode auf der Memory<T>-Instanz, die „void“ zurückgibt, beginnt, wenn die Methode aufgerufen wird, und endet, wenn die Methode beendet wird. Schauen Sie sich das folgende Beispiel an, in dem basierend auf der Eingabe von der Konsole ein Log aufgerufen wird.

// <Snippet1>
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;
    }
}
// </Snippet1>

// Possible implementation of Log:
    // private static void Log(ReadOnlyMemory<char> message)
    // {
    //     Console.WriteLine(message);
    // }

Wenn Log eine vollständig synchrone Methode ist, verhält sich dieser Code erwartungsgemäß, da zu jedem Zeitpunkt nur ein aktiver Verbraucher der Speicherinstanz vorhanden ist. Stellen Sie sich aber stattdessen vor, dass Log diese Implementierung hat.

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

In dieser Implementierung verstößt Log gegen die Leasedauer, weil immer noch versucht wird, die Memory<T>-Instanz im Hintergrund zu verwenden, nachdem die ursprüngliche Methode zurückgegeben wurde. Die Main Methode könnte den Puffer mutieren, während Log versucht wird, ihn zu lesen, was zu einer Datenbeschädigung führen könnte.

Es gibt mehrere Möglichkeiten, dieses Problem zu lösen:

  • Die Log-Methode kann ein Task anstelle eines void zurückgeben, wie es in der folgenden Implementierung der Log-Methode der Fall ist.

    // 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 kann stattdessen wie folgt implementiert werden:

    // 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 Nr. 4: Wenn Ihre Methode einen Arbeitsspeicher<T> akzeptiert und eine Aufgabe zurückgibt, dürfen Sie die Memory<T-Instanz> nicht verwenden, nachdem die Aufgabe in einen Terminalzustand übergestellt wurde.

Dies ist nur die asynchrone Variante von Rule #3. Die Log Methode aus dem vorherigen Beispiel kann wie folgt geschrieben werden, um diese Regel einzuhalten:

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

Hier bedeutet "Terminalstatus", dass der Vorgang zu einem abgeschlossenen, fehlerhaften oder abgebrochenen Zustand wechselt. Anders ausgedrückt bedeutet „Endzustand“ „alles, was das Warten auf das Auslösen oder Fortsetzen der Ausführung verursachen würde“.

Dieser Leitfaden gilt für Methoden, die Task, Task<TResult> oder ValueTask<TResult> oder einen ähnlichen Typ zurückgeben.

Regel Nr. 5: Wenn Ihr Konstruktor Memory<T> als Parameter akzeptiert, werden Instanzmethoden für das erstellte Objekt als Verbraucher der Memory<T>-Instanz angenommen.

Betrachten Sie das folgenden Beispiel:

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

Hier akzeptiert der Konstruktor einen OddValueExtractor als Konstruktorparameter, daher ist der ReadOnlyMemory<int> Konstruktor selbst ein Consumer der ReadOnlyMemory<int> Instanz, und alle Instanzmethoden für den zurückgegebenen Wert sind auch Consumer der ursprünglichen ReadOnlyMemory<int> Instanz. Dies bedeutet, dass TryReadNextOddValue die ReadOnlyMemory<int> Instanz verwendet wird, obwohl die Instanz nicht direkt an die TryReadNextOddValue Methode übergeben wird.

Regel #6: Wenn Sie über eine setzbare Eigenschaft vom Typ Memory<T (oder eine entsprechende Instanzmethode) für Ihren Typ verfügen, werden die Instanzmethoden dieses Objekts als Benutzer der Memory>T-Instanz< betrachtet.

Dies ist wirklich nur eine Variante von Regel #5. Diese Regel ist vorhanden, da Eigenschaftssatzer oder gleichwertige Methoden angenommen werden, um ihre Eingaben zu erfassen und beizubehalten, sodass Instanzmethoden für dasselbe Objekt den erfassten Zustand verwenden können.

Im folgenden Beispiel wird diese Regel ausgelöst:

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: Wenn Sie einen IMemoryOwner<T>-Verweis haben, müssen Sie ihn irgendwann verwerfen oder den Besitz übertragen (aber nicht beides).

Da eine Memory<T>-Instanz entweder durch verwalteten oder nicht verwalteten Speicher unterstützt werden kann, muss der Besitzer Dispose auf IMemoryOwner<T> aufrufen, wenn die Arbeit an der Memory<T>-Instanz abgeschlossen ist. Alternativ kann der Besitzer den Besitz der IMemoryOwner<T> Instanz an eine andere Komponente übertragen, an deren Zeitpunkt die Beschaffungskomponente zur entsprechenden Zeit für das Aufrufen Dispose verantwortlich wird (mehr dazu später).

Fehler beim Aufrufen der Dispose Methode für eine IMemoryOwner<T> Instanz können zu nicht verwalteten Speicherlecks oder einer anderen Leistungsbeeinträchtigung führen.

Diese Regel gilt auch für Code, der Factory-Methoden wie MemoryPool<T>.Rent aufruft. Der Aufrufer wird zum Besitzer der zurückgegebenen IMemoryOwner<T> Instanz und ist für das Löschen der Instanz verantwortlich, wenn sie fertig gestellt ist.

Regel #8: Wenn Sie über einen IMemoryOwner<T-Parameter> in Ihrer API-Oberfläche verfügen, akzeptieren Sie den Besitz dieser Instanz.

Durch Akzeptieren einer Instanz dieses Typs wird signalisiert, dass Ihre Komponente den Besitz dieser Instanz übernehmen möchte. Ihre Komponente wird gemäß Regel Nr. 7 für die ordnungsgemäße Entsorgung verantwortlich.

Jede Komponente, die den Besitz der IMemoryOwner<T> Instanz an eine andere Komponente überträgt, sollte diese Instanz nach Abschluss des Methodenaufrufs nicht mehr verwenden.

Von Bedeutung

Wenn Ihr Konstruktor IMemoryOwner<T> als Parameter akzeptiert, sollte sein Typ IDisposable implementieren, und Ihre Dispose Methode sollte Dispose auf dem Objekt IMemoryOwner<T> aufrufen.

Regel Nr. 9: Wenn Sie eine synchrone P/Invoke-Methode umschließen, sollte Ihre API Span<T> als Parameter akzeptieren.

Gemäß Regel Nr. 1 ist Span<T> im Allgemeinen der richtige Typ zur Verwendung für synchrone APIs. Sie können Span<T>-Instanzen über das Schlüsselwort fixed anheften, wie im folgenden Beispiel.

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

Im vorherigen Beispiel kann null sein, pbData wenn die Eingabespanne beispielsweise leer ist. Wenn die exportierte Methode absolut erfordert, dass dies pbData nicht null ist, auch wenn cbData 0 ist, kann die Methode wie folgt implementiert werden:

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: Wenn Sie eine asynchrone P/Invoke-Methode umschließen, sollte Ihre API Memory<T> als Parameter akzeptieren.

Da Sie das fixed Schlüsselwort nicht für asynchrone Vorgänge verwenden können, verwenden Sie die Memory<T>.Pin Methode, um Memory<T> Instanzen anzuheften, unabhängig von der Art des zusammenhängenden Speichers, den die Instanz darstellt. Das folgende Beispiel zeigt, wie Sie diese API verwenden, um einen asynchronen P/Invoke-Aufruf auszuführen.

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

Siehe auch