A Memória<T> és a Span<T> használati irányelvei

A .NET számos olyan típust tartalmaz, amelyek a memória tetszőleges összefüggő régióját jelölik. Span<T> és ReadOnlySpan<T> könnyű memóriapufferek, amelyek a felügyelt vagy nem felügyelt memóriára mutató hivatkozásokat törik körbe. Mivel ezek a típusok csak a veremen tárolhatók, nem alkalmasak olyan helyzetekre, mint az aszinkron metódushívások. A probléma megoldásához a .NET 2.1 hozzáadott néhány további típust, köztük Memory<T>a , ReadOnlyMemory<T>, IMemoryOwner<T>és MemoryPool<T>. Memory<T> Ehhez hasonlóan Span<T>a kapcsolódó típusok felügyelt és nem felügyelt memóriával is támogatottak. Ellentétben Span<T>a Memory<T> felügyelt halomtárban tárolható.

Memory<T> Mindkettő Span<T> burkoló a folyamatokban használható strukturált adatok pufferei felett. Vagyis úgy lettek kialakítva, hogy az adatok egy része vagy egésze hatékonyan továbbítható legyen a folyamat összetevőinek, amelyek feldolgozhatják és tetszés szerint módosíthatják a puffert. Mivel Memory<T> és a kapcsolódó típusok több összetevővel vagy több szálon is elérhetők, fontos, hogy a szabványos használati irányelveket követve robusztus kódot állítsunk elő.

Tulajdonosok, fogyasztók és élettartam-kezelés

A pufferek átadhatók az API-k között, és néha több szálból is elérhetők, ezért vegye figyelembe a pufferek élettartamának kezelését. Három alapvető fogalom létezik:

  • Tulajdonjog. A pufferpéldány tulajdonosa felelős az élettartam-kezelésért, beleértve a puffer megsemmisítését, ha már nincs használatban. Minden puffernek egyetlen tulajdonosa van. Általában a tulajdonos az az összetevő, amely létrehozta a puffert, vagy amely a puffert egy gyárból kapta. A tulajdonjog át is ruházható; Az A összetevő vissza tudja adni a puffer vezérlését a B összetevőnek, ekkor előfordulhat, hogy az A összetevő már nem használja a puffert, és a B összetevő lesz a felelős a puffer megsemmisítéséért, ha már nincs használatban.

  • Használatalapú. A pufferpéldány felhasználója olvasással és esetleg írással használhatja a pufferpéldányt. A pufferek egyszerre csak egy fogyasztóval rendelkezhetnek, kivéve, ha valamilyen külső szinkronizálási mechanizmus van megadva. A puffer aktív felhasználója nem feltétlenül a puffer tulajdonosa.

  • Bérlet. A bérlet az az időtartam, amellyel egy adott összetevő a puffer fogyasztója lehet.

Az alábbi pszeudokód-példa ezt a három fogalmat szemlélteti. Buffera pszeudokód egy vagy Span<T> több Memory<T> típusú Charpuffert jelöl. A Main metódus példányosítja a puffert, meghívja a WriteInt32ToBuffer metódust egy egész szám sztringképének a pufferbe való írására, majd meghívja a DisplayBufferToConsole metódust a puffer értékének megjelenítésére.

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

A Main metódus létrehozza a puffert, és a tulajdonosa is. Main Ezért felelős a puffer megsemmisítéséért, ha már nincs használatban. A pszeudokód ezt egy metódus meghívásával Destroy szemlélteti a pufferben. (Sem a módszer, sem Memory<T>Span<T> a Destroy tényleges. A tényleges kód példákat a cikk későbbi részében tekintheti meg.)

A puffernek két fogyasztója van, WriteInt32ToBuffer és DisplayBufferToConsole. Egyszerre csak egy fogyasztó van (először WriteInt32ToBuffer, majd DisplayBufferToConsole), és egyik fogyasztó sem rendelkezik a pufferrel. Vegye figyelembe azt is, hogy ebben a kontextusban a "fogyasztó" nem jelenti a puffer írásvédett nézetét; a felhasználók módosíthatják a puffer tartalmát, ahogyan WriteInt32ToBuffer az is, ha olvasási/írási nézetet kapnak a pufferről.

A WriteInt32ToBuffer metódus a metódushívás kezdete és a metódus visszatérési ideje között bérletet tartalmaz (felhasználhatja) a puffert. Hasonlóképpen, DisplayBufferToConsole van egy bérlete a pufferen a végrehajtás során, és a bérlet felszabadul, amikor a metódus visszateker. (A bérletkezeléshez nincs API, a "bérlet" fogalmi kérdés.)

Memória<T> és a tulajdonosi/fogyasztói modell

Ahogy a Tulajdonosok, a fogyasztók és az élettartam-kezelés szakasz megjegyzi, a puffernek mindig van tulajdonosa. A .NET két tulajdonosi modellt támogat:

  • Egy olyan modell, amely támogatja az önálló tulajdonjogot. A puffer teljes élettartama egyetlen tulajdonossal rendelkezik.

  • A tulajdonosátadást támogató modell. A puffer tulajdonjoga átruházható az eredeti tulajdonostól (létrehozójától) egy másik összetevőre, amely ezután felelőssé válik a puffer élettartam-kezeléséért. Ez a tulajdonos átadhatja a tulajdonjogot egy másik összetevőnek, és így tovább.

Az interfész használatával System.Buffers.IMemoryOwner<T> explicit módon kezelheti a puffer tulajdonjogát. IMemoryOwner<T> mindkét tulajdonosi modellt támogatja. A puffert a hivatkozással IMemoryOwner<T> rendelkező összetevő birtokolja. Az alábbi példa egy példányt IMemoryOwner<T> használ egy puffer tulajdonjogának Memory<T> tükrözésére.

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

Ezt a példát a következő utasítással usingis megírhatjuk:

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

Ebben a kódban:

  • A Main metódus tartalmazza a példányra IMemoryOwner<T> mutató hivatkozást, így a Main metódus a puffer tulajdonosa.

  • A WriteInt32ToBuffer metódusok nyilvános DisplayBufferToConsole API-ként fogadnak el Memory<T> . Ezért ők a puffer felhasználói. Ezek a metódusok egyenként fogyasztják a puffert.

Bár a WriteInt32ToBuffer metódus célja egy érték írása a pufferbe, a DisplayBufferToConsole metódusnak nem célja. Ennek tükrözéséhez elfogadhatott volna egy típus ReadOnlyMemory<T>argumentumot . További információ: ReadOnlyMemory<T>2. szabály: A ReadOnlySpan<T> vagy a ReadOnlyMemory<T> használata, ha a puffer írásvédettnek kell lennie.

"Tulajdonos nélküli" Memória<T-példányok>

A példányt Memory<T> a használata IMemoryOwner<T>nélkül is létrehozhatja. Ebben az esetben a puffer tulajdonjoga nem explicit, hanem implicit, és csak az egytulajdonosi modell támogatott. Ezt a következőképpen teheti meg:

  • Az egyik konstruktor meghívása Memory<T> közvetlenül, a T[]következő példának megfelelően.

  • A String.AsMemory bővítmény metódus meghívása egy ReadOnlyMemory<char> példány létrehozásához.

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

A példányt kezdetben létrehozó Memory<T> metódus a puffer implicit tulajdonosa. A tulajdonjog nem ruházható át más összetevőre, mert nincs IMemoryOwner<T> olyan példány, amely megkönnyítené az átadást. (Alternatív megoldásként azt is el lehet képzelni, hogy a futtatókörnyezet szemétgyűjtője birtokolja a puffert, és minden metódus csak a puffert használja.)

Használatra vonatkozó útmutatások

Mivel a memóriablokkok tulajdonosa, de több összetevőnek is átadják, amelyek közül néhány egyszerre egy adott memóriablokkon is működhet, fontos, hogy irányelveket állapítsunk meg mind a kettő Memory<T>Span<T>és a . Az irányelvekre azért van szükség, mert lehetséges, hogy egy összetevő:

  • Őrizze meg a memóriablokkra mutató hivatkozást, miután a tulajdonosa kiadta.

  • A pufferben lévő adatok sérülésének folyamatában egy másik összetevő működésével egy időben működjön.

  • Bár a veremelt jellege Span<T> optimalizálja a teljesítményt, és a memóriablokkokon való üzemeltetéshez az előnyben részesített típust teszi Span<T> lehetővé, bizonyos főbb korlátozásokra is vonatkozik Span<T> . Fontos tudni, hogy mikor és mikor érdemes használni Span<T>Memory<T>.

Az alábbiakban a sikeres használatra Memory<T> és a kapcsolódó típusokra vonatkozó javaslatainkat tekintjük át. Útmutatás, amely másként Memory<T> nem szerepel, és Span<T> azokra is vonatkozik és ReadOnlySpan<T> azokra is vonatkozikReadOnlyMemory<T>.

1. szabály: Szinkron API-k esetén a T<memória helyett a Span<T>> paramétert használja, ha lehetséges.

Span<T> sokoldalúbb, mint Memory<T> az egybefüggő memóriapufferek szélesebb választéka. Span<T> is jobb teljesítményt nyújt, mint Memory<T>. Végül a Memory<T>.Span tulajdonság használatával átalakíthat egy példányt Memory<T>Span<T>egy , bár a T-a-Memória<<> T> konvertálása nem lehetséges. Ha tehát a hívónak van példánya Memory<T> , akkor a metódusokat paraméterekkel Span<T> is meghívhatják.

Ha típus Memory<T> helyett típus típusú Span<T> paramétert használ, az is segít a helyes használatban lévő metódus implementációjának megírásában. Automatikusan lekérheti a fordítási idő ellenőrzését, hogy ne kísérelje meg elérni a puffert a metódus bérletén túl (erről később olvashat bővebben).

Előfordulhat, hogy paraméter helyett Span<T> paramétert Memory<T> kell használnia, még akkor is, ha teljesen szinkron. Lehet, hogy egy olyan API, amelytől függ, csak Memory<T> argumentumokat fogad el. Ez rendben van, de vegye figyelembe a szinkron használat Memory<T> során felmerülő kompromisszumokat.

2. szabály: Használja a ReadOnlySpan<T> vagy a ReadOnlyMemory T> parancsot<, ha a puffer írásvédettnek kell lennie.

A korábbi példákban a metódus csak a DisplayBufferToConsole pufferből olvas, nem módosítja a puffer tartalmát. A metódus-aláírást a következőre kell módosítani.

void DisplayBufferToConsole(ReadOnlyMemory<char> buffer);

Valójában, ha kombináljuk ezt a szabályt és az 1. szabályt, még jobbat tehetünk, és az alábbiak szerint írhatjuk át a metódus aláírását:

void DisplayBufferToConsole(ReadOnlySpan<char> buffer);

A DisplayBufferToConsole metódus mostantól gyakorlatilag minden elképzelhető puffertípussal működik: T[]a stackalloc-tal lefoglalt tárolóval stb. Akár közvetlenül is átadhat belőle!String További információ: GitHub-probléma dotnet/docs #25551.

3. szabály: Ha a metódus elfogadja a Memória<T-t> , és visszaadja void, a metódus visszatérése után nem használhatja a Memory<T-példányt> .

Ez a korábban említett "bérlet" koncepcióhoz kapcsolódik. Egy érvénytelenítő metódus bérlete a példányon a Memory<T> metódus beírásakor kezdődik, és a metódus kilépésével ér véget. Vegyük az alábbi példát, amely a konzol bemenete alapján egy hurokban hívja meg a hívásokat Log .

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

Ha Log egy teljes mértékben szinkron metódus, ez a kód a várt módon fog viselkedni, mivel a memóriapéldánynak egyszerre csak egy aktív felhasználója van. De képzelje el inkább, hogy Log ez a megvalósítás.

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

Ebben az implementációban megsérti a bérletét, Log mert az eredeti metódus visszaadása után is megpróbálja használni a Memory<T> példányt a háttérben. A Main metódus mutálhatja a puffert, miközben Log megpróbál olvasni belőle, ami adatsérülést okozhat.

A probléma megoldásának több módja is van:

  • A Log metódus visszaadhatja Task ahelyett void, hogy a metódus következő implementációját követve Log ad vissza.

    // 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 ehelyett az alábbiak szerint valósítható meg:

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

4. szabály: Ha a metódus elfogad egy Memória<T-t> , és egy feladatot ad vissza, akkor a Feladat terminálállapotra váltása után nem szabad használni a Memory<T-példányt> .

Ez csak a 3. szabály aszinkron változata. A Log korábbi példában szereplő módszer a következő módon írható meg, hogy megfeleljen ennek a szabálynak:

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

Itt a "terminálállapot" azt jelenti, hogy a tevékenység befejezett, hibás vagy megszakított állapotra vált. Más szóval a "terminálállapot" azt jelenti, hogy "bármi, ami a végrehajtásra várna vagy folytatódna".

Ez az útmutató azokra a metódusokra vonatkozik, amelyek visszaadják , Task<TResult>ValueTask<TResult>vagy bármilyen hasonló típust ad visszaTask.

5. szabály: Ha a konstruktor paraméterként fogadja el a Memory<T-t> , akkor a rendszer feltételezi, hogy a létrehozott objektum példánymetódusai a Memory<T-példány> felhasználói.

Vegyük a következő példát:

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

Itt a OddValueExtractor konstruktor konstruktorparaméterként ReadOnlyMemory<int> fogadja el a konstruktort, így maga a konstruktor a ReadOnlyMemory<int> példány fogyasztója, és a visszaadott érték összes példánymetóra egyben az eredeti ReadOnlyMemory<int> példány felhasználói is. Ez azt jelenti, hogy TryReadNextOddValue a példányt ReadOnlyMemory<int> akkor is felhasználja, ha a példányt nem továbbítja közvetlenül a TryReadNextOddValue metódusnak.

6. szabály: Ha beállított Memória<T> típusú tulajdonsága (vagy ezzel egyenértékű példánymetódusa) van a típuson, a rendszer feltételezi, hogy az objektum példánymetódusai a Memory<T-példány> felhasználói.

Ez valójában csak az 5. szabály egyik változata. Ez a szabály azért létezik, mert a rendszer feltételezi, hogy a tulajdonsághalmazok vagy az azzal egyenértékű metódusok rögzítik és megőrzik a bemeneteiket, így az ugyanazon az objektumon lévő példánymetódusok használhatják a rögzített állapotot.

A következő példa aktiválja ezt a szabályt:

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

7. szabály: Ha IMemoryOwner<T-hivatkozással> rendelkezik, akkor valamikor el kell rendelkeznie tőle, vagy át kell ruháznia a tulajdonjogát (de mindkettőt nem).

Mivel egy Memory<T> példányt felügyelt vagy nem felügyelt memória is segíthet, a tulajdonosnak fel kell hívnia DisposeIMemoryOwner<T> , ha a Memory<T> példányon végzett munka befejeződött. Másik lehetőségként a tulajdonos átruházhatja a IMemoryOwner<T> példány tulajdonjogát egy másik összetevőre, amely időpontban a beszerző összetevő lesz a felelős a megfelelő időpontban történő hívásért Dispose (erről bővebben később).

A metódus meghívásának Dispose elmulasztása egy IMemoryOwner<T> példányon nem felügyelt memóriaszivárgáshoz vagy más teljesítménycsökkenéshez vezethet.

Ez a szabály olyan kódra is vonatkozik, amely az olyan gyári metódusokat hívja meg, mint a MemoryPool<T>.Rent. A hívó lesz a visszaadott IMemoryOwner<T> tulajdonos, és felelős a példány véglegesítéséért.

8. szabály: Ha egy IMemoryOwner<T> paraméter van az API-felületen, elfogadja a példány tulajdonjogát.

Ha elfogad egy ilyen típusú példányt, az azt jelzi, hogy az összetevő a példány tulajdonjogát kívánja átvenni. Az összetevő a 7. szabálynak megfelelően felelőssé válik a megfelelő ártalmatlanításért.

Minden olyan összetevő, amely a IMemoryOwner<T> példány tulajdonjogát egy másik összetevőre továbbítja, a metódushívás befejeződése után már nem használhatja ezt a példányt.

Fontos

Ha a konstruktor IMemoryOwner<T> paraméterként fogadja el, a típusnak implementálnia IDisposablekell, és a Dispose metódusnak meg kell hívnia Dispose az IMemoryOwner<T> objektumot.

9. szabály: Ha szinkron p/invoke metódust burkol, az API-nak a Span<T> paramétert kell elfogadnia paraméterként.

Az 1 Span<T> . szabály szerint általában a megfelelő típus a szinkron API-khoz. A példányokat a fixed kulcsszón keresztül rögzíthetiSpan<T>, ahogyan az alábbi példában is látható.

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

Az előző példában pbData null értékű lehet, ha például a bemeneti tartomány üres. Ha az exportált módszerhez feltétlenül nem null értékűnek kell pbData lennie, még akkor is, ha cbData 0, a metódus az alábbiak szerint implementálható:

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

10. szabály: Ha aszinkron p/invoke metódust burkol, az API-nak paraméterként el kell fogadnia a Memory<T-t> .

Mivel a kulcsszó nem használható aszinkron fixed műveletekben, a Memory<T>.Pin metódussal rögzíthet Memory<T> példányokat, függetlenül attól, hogy a példány milyen összefüggő memóriát képvisel. Az alábbi példa bemutatja, hogyan használhatja ezt az API-t aszinkron p/invoke hívás végrehajtására.

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

Lásd még