Pokyny k používání paměti< a> SpanT<>

.NET Core obsahuje řadu typů, které představují libovolnou souvislou oblast paměti. .NET Core 2.0 zavedl Span<T> a ReadOnlySpan<T>, což jsou zjednodušené vyrovnávací paměti, které zabalují odkazy na spravovanou nebo nespravovanou paměť. Vzhledem k tomu, že tyto typy mohou být uloženy pouze v zásobníku, jsou nevhodné pro řadu scénářů, včetně asynchronních volání metod. .NET Core 2.1 přidává řadu dalších typů, včetně Memory<T>, , ReadOnlyMemory<T>IMemoryOwner<T>a MemoryPool<T>. Memory<T> Podobně jako Span<T>u souvisejících typů je možné zálohovat spravovanou i nespravovanou pamětí. Na rozdíl od Span<T>toho Memory<T> lze uložit na spravované haldě.

Memory<T> Obálky Span<T> jsou obálky vyrovnávací paměti strukturovaných dat, které lze použít v kanálech. To znamená, že jsou navrženy tak, aby některá nebo všechna data byla efektivně předána komponentám v kanálu, což je může zpracovat a volitelně upravit vyrovnávací paměť. Vzhledem k tomu Memory<T> , že k souvisejícím typům je možné přistupovat více komponentami nebo několika vlákny, je důležité, aby vývojáři dodržovali některé standardní pokyny k používání, aby vytvořili robustní kód.

Vlastníci, spotřebitelé a správa životnosti

Vzhledem k tomu, že vyrovnávací paměti se dají předávat mezi rozhraními API, a protože k vyrovnávacím pamětím je někdy možné přistupovat z více vláken, je důležité zvážit správu životnosti. Existují tři základní koncepty:

  • Vlastnictví. Vlastník instance vyrovnávací paměti zodpovídá za správu životnosti, včetně zničení vyrovnávací paměti, když se už nepoužívá. Všechny vyrovnávací paměti mají jednoho vlastníka. Obecně platí, že vlastníkem je komponenta, která vytvořila vyrovnávací paměť nebo která přijala vyrovnávací paměť z továrny. Vlastnictví lze také převést; Komponenta-A může relinquish řídit vyrovnávací paměť na Component-B, kdy komponenta-A již nemusí používat vyrovnávací paměť, a Komponenta-B se stane zodpovědnou za zničení vyrovnávací paměti, když se už nepoužívá.

  • Consumption. Příjemce instance vyrovnávací paměti může používat instanci vyrovnávací paměti čtením z ní a případně zápisem do ní. Vyrovnávací paměti můžou mít najednou jednoho příjemce, pokud není k dispozici nějaký externí synchronizační mechanismus. Aktivní příjemce vyrovnávací paměti nemusí nutně být vlastníkem vyrovnávací paměti.

  • Zapůjčení. Zapůjčení je doba, po kterou může být konkrétní komponenta příjemcem vyrovnávací paměti.

Následující příklad pseudokódu ilustruje tyto tři koncepty. Buffer v pseudokódu představuje Memory<T> nebo Span<T> vyrovnávací paměť typu Char. Metoda Main vytvoří instanci vyrovnávací paměti, volá metodu WriteInt32ToBuffer pro zápis řetězcové reprezentace celého čísla do vyrovnávací paměti a potom volá metodu DisplayBufferToConsole pro zobrazení hodnoty vyrovnávací paměti.

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

Metoda Main vytvoří vyrovnávací paměť a tak je jeho vlastníkem. Proto je zodpovědný za zničení vyrovnávací paměti, Main když už se nepoužívá. Pseudokód to ilustruje voláním Destroy metody ve vyrovnávací paměti. (Ani Memory<T>Span<T> ve skutečnosti nemá metodu Destroy . Skutečné příklady kódu uvidíte později v tomto článku.)

Vyrovnávací paměť má dva příjemce a WriteInt32ToBufferDisplayBufferToConsole. Najednou existuje pouze jeden spotřebitel (první WriteInt32ToBuffer, pak DisplayBufferToConsole) a ani jeden z příjemců nevlastní vyrovnávací paměť. Všimněte si také, že "příjemce" v tomto kontextu neznamená zobrazení vyrovnávací paměti jen pro čtení; uživatelé můžou obsah vyrovnávací paměti upravit stejně WriteInt32ToBuffer jako v případě, že se zobrazí zobrazení vyrovnávací paměti pro čtení a zápis.

Metoda WriteInt32ToBuffer má zapůjčení (může spotřebovat) vyrovnávací paměť mezi zahájením volání metody a časem, kdy metoda vrátí. DisplayBufferToConsole Podobně má zapůjčení vyrovnávací paměti během jeho provádění a zapůjčení se uvolní, když se metoda uvolní. (Pro správu zapůjčení neexistuje žádné rozhraní API; "zapůjčení" je koncepční záležitost.)

MemoryT<> a model vlastníka/příjemce

Jako poznámky k oddílu Vlastníci, spotřebitelé a správa životnosti má vyrovnávací paměť vždy vlastníka. .NET Core podporuje dva modely vlastnictví:

  • Model, který podporuje jedno vlastnictví. Vyrovnávací paměť má jednoho vlastníka po celou dobu života.

  • Model, který podporuje převod vlastnictví. Vlastnictví vyrovnávací paměti lze přenést od původního vlastníka (jeho tvůrce) do jiné součásti, která pak bude zodpovědná za správu životnosti vyrovnávací paměti. Tento vlastník může převést vlastnictví na jinou komponentu atd.

Rozhraní System.Buffers.IMemoryOwner<T> slouží k explicitní správě vlastnictví vyrovnávací paměti. IMemoryOwner<T> podporuje oba modely vlastnictví. Komponenta IMemoryOwner<T> , která má odkaz, vlastní vyrovnávací paměť. Následující příklad používá IMemoryOwner<T> instanci k vyjádření vlastnictví Memory<T> vyrovnávací paměti.

using System;
using System.Buffers;

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

        Console.Write("Enter a number: ");
        try {
            var value = Int32.Parse(Console.ReadLine());

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

Můžeme také napsat tento příklad pomocí using:

using System;
using System.Buffers;

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

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

V tomto kódu:

  • Metoda Main obsahuje odkaz na IMemoryOwner<T> instanci, takže Main metoda je vlastníkem vyrovnávací paměti.

  • Metody WriteInt32ToBuffer a DisplayBufferToConsole metody přijímají Memory<T> jako veřejné rozhraní API. Proto jsou příjemci vyrovnávací paměti. A spotřebovávají ho jen jednou po druhém.

I když je WriteInt32ToBuffer metoda určená k zápisu hodnoty do vyrovnávací paměti, DisplayBufferToConsole metoda není. Aby to bylo možné odrážet, mohlo by to přijmout argument typu ReadOnlyMemory<T>. Další informace o ReadOnlyMemory<T>pravidle č. 2: Použití readOnlySpanT< nebo ReadOnlyMemoryT><>, pokud má být vyrovnávací paměť jen pro čtení.

Instance MemoryT<> bez vlastníka

Instanci můžete vytvořit Memory<T> bez použití IMemoryOwner<T>. V tomto případě je vlastnictví vyrovnávací paměti implicitní, nikoli explicitní a podporuje se pouze model s jedním vlastníkem. Můžete to udělat takto:

  • Volání jednoho z Memory<T> konstruktorů přímo, předávání , T[]jako v následujícím příkladu.

  • Volání String.AsMemory rozšiřující metoda pro vytvoření ReadOnlyMemory<char> instance.

using System;

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

        Console.Write("Enter a number: ");
        var value = Int32.Parse(Console.ReadLine());

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

Metoda, která původně vytvoří Memory<T> instanci, je implicitní vlastník vyrovnávací paměti. Vlastnictví nelze přenést do žádné jiné komponenty, protože neexistuje žádná IMemoryOwner<T> instance pro usnadnění převodu. (Jako alternativu si také můžete představit, že uvolňování paměti modulu runtime vlastní vyrovnávací paměť a všechny metody pouze spotřebovávají vyrovnávací paměť.)

Pokyny k používání

Vzhledem k tomu, že blok paměti je vlastněný, ale má být předán do více komponent, některé z nich mohou fungovat na konkrétním bloku paměti současně, je důležité stanovit pokyny pro použití obou Memory<T> a Span<T>. Pokyny jsou nezbytné, protože:

  • Komponentu je možné zachovat odkaz na blok paměti po vydání jeho vlastníka.

  • Komponenta může pracovat se vyrovnávací pamětí současně s tím, že na ní pracuje jiná komponenta, v procesu poškození dat ve vyrovnávací paměti.

  • I když je typ optimalizace výkonu Span<T> přidělený zásobníkem a dává Span<T> upřednostňovaný typ pro provoz na paměťovém bloku, má Span<T> také určitá hlavní omezení. Je důležité vědět, kdy použít Span<T> a kdy použít Memory<T>.

Následují naše doporučení pro úspěšné použití Memory<T> a související typy. Pokyny, které platí Memory<T> pro a Span<T> platí i pro ReadOnlyMemory<T> a ReadOnlySpan<T> pokud výslovně nezaznamenáme jinak.

Pravidlo č. 1: Pro synchronní rozhraní API použijte jako parametr SpanT< místo MemoryT><>, pokud je to možné.

Span<T> je univerzálnější než Memory<T> a může představovat širší škálu souvislých vyrovnávacích pamětí paměti. Span<T> také nabízí lepší výkon než Memory<T>. Nakonec můžete použít Memory<T>.Span vlastnost k převodu Memory<T> instance na Span<T>, i když< spanT-to-MemoryT><> převod není možné. Takže pokud se volajícím stane, že mají Memory<T> instanci, budou moct přesto volat metody s Span<T> parametry.

Použití parametru typu místo typu Span<T>Memory<T> vám také pomůže napsat správnou implementaci metody s využitím. Automaticky získáte kontroly času kompilace, abyste se ujistili, že se nebudete pokoušet o přístup k vyrovnávací paměti nad rámec zapůjčení vaší metody (více o tom později).

Někdy budete muset místo parametru Memory<T>Span<T> použít parametr, i když jste plně synchronní. Možná rozhraní API, na které závisíte, přijímá pouze Memory<T> argumenty. Je to v pořádku, ale mějte na paměti kompromisy, které se týkají synchronního používání Memory<T> .

Pravidlo č. 2: Použijte ReadOnlySpanT<> nebo ReadOnlyMemoryT><, pokud má být vyrovnávací paměť jen pro čtení.

V předchozích příkladech DisplayBufferToConsole metoda čte pouze z vyrovnávací paměti; neupravuje obsah vyrovnávací paměti. Podpis metody by měl být změněn na následující.

void DisplayBufferToConsole(ReadOnlyMemory<char> buffer);

Ve skutečnosti, pokud zkombinujeme toto pravidlo a pravidlo č. 1, můžeme ještě lépe a přepsat podpis metody následujícím způsobem:

void DisplayBufferToConsole(ReadOnlySpan<char> buffer);

Metoda DisplayBufferToConsole teď funguje s prakticky každým typem vyrovnávací paměti, který je možné představit: T[], úložiště přidělené stackalloc atd. Dokonce můžete předat String přímo do něj! Další informace najdete v tématu GitHub problém s dotnet/docs #25551.

Pravidlo č. 3: Pokud vaše metoda přijímá MemoryT<> a vrací void, nesmíte použít instanci MemoryT<> po vrácení metody.

To souvisí s konceptem "zapůjčení", který jsme zmínili dříve. Zapůjčení metody vrácení void v Memory<T> instanci začíná při zadání metody a končí po ukončení metody. Podívejte se na následující příklad, který volá Log smyčku založenou na vstupu z konzoly.

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)
            {
                int value = Int32.Parse(Console.ReadLine());
                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;
    }
}

Pokud Log je plně synchronní metoda, tento kód se bude chovat podle očekávání, protože v daném okamžiku existuje pouze jeden aktivní příjemce instance paměti. Představte si ale, že Log tuto implementaci má.

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

V této implementaci porušuje jeho zapůjčení, Log protože se stále pokouší použít Memory<T> instanci na pozadí po vrácení původní metody. Metoda Main by mohla ztlumit vyrovnávací paměť při Log pokusu o čtení z něj, což může vést k poškození dat.

Existuje několik způsobů, jak to vyřešit:

  • Metoda Log může vrátit Task místo void, protože následující implementace Log metody.

    // 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 lze místo toho implementovat takto:

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

Pravidlo č. 4: Pokud vaše metoda přijme MemoryT<> a vrátí úlohu, nesmíte po přechodu úlohy na terminálový stav použít instanci MemoryT<>.

Toto je jenom asynchronní varianta pravidla č. 3. Metodu Log z předchozího příkladu je možné zapsat následujícím způsobem, aby vyhovovala tomuto pravidlu:

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

V této části "stav terminálu" znamená, že úloha přejde na dokončený, chybný nebo zrušený stav. Jinými slovy" "terminálový stav" znamená "cokoli, co by způsobilo, že čeká na vyvolání nebo pokračování provádění.".

Tyto pokyny platí pro metody, které vracejí Task, Task<TResult>, ValueTask<TResult>nebo jakýkoli podobný typ.

Pravidlo č. 5: Pokud konstruktor přijímá funkci MemoryT>< jako parametr, předpokládá se, že metody instance na vytvořeném objektu budou příjemci instance MemoryT<>.

Uvažujte následující příklad:

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 Zde konstruktor přijímá ReadOnlyMemory<int> jako parametr konstruktoru, takže samotný konstruktor je příjemcem ReadOnlyMemory<int> instance a všechny metody instance na vrácenou hodnotu jsou také příjemci původní ReadOnlyMemory<int> instance. To znamená, že TryReadNextOddValue využívá ReadOnlyMemory<int> instanci, i když instance není předána přímo metodě TryReadNextOddValue .

Pravidlo č. 6: Pokud máte nastavenou vlastnost typu MemoryT>< (nebo ekvivalentní metodu instance), metody instance na tomto objektu se předpokládají jako příjemci instance MemoryT<>.

Toto je opravdu jen varianta pravidla č. 5. Toto pravidlo existuje, protože settery vlastností nebo ekvivalentní metody se předpokládají zachytávání a zachování jejich vstupů, takže metody instance na stejném objektu mohou využívat zachycený stav.

Následující příklad aktivuje toto pravidlo:

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

Pravidlo č. 7: Pokud máte odkaz IMemoryOwnerT<>, musíte ho v určitém okamžiku zlikvidovat nebo převést jeho vlastnictví (ale ne obojí).

Memory<T> Vzhledem k tomu, že instance může být zajištěna spravovanou nebo nespravovanou pamětí, musí vlastník při dokončení práce na Memory<T> instanci volat DisposeIMemoryOwner<T>. Případně může vlastník převést vlastnictví IMemoryOwner<T> instance na jinou komponentu, v jakém okamžiku se získání komponenty stane zodpovědným za volání Dispose v příslušné době (více o tom později).

Selhání volání Dispose metody v IMemoryOwner<T> instanci může vést k nespravované nespravované paměti nevracení nebo jinému snížení výkonu.

Toto pravidlo platí také pro kód, který volá metody továrny, jako je MemoryPool<T>.Rent. Volající se stane vlastníkem vrácené IMemoryOwner<T> instance a po dokončení je zodpovědný za vystavení instance.

Pravidlo č. 8: Pokud máte v povrchu rozhraní API parametr IMemoryOwnerT<>, přijímáte vlastnictví této instance.

Přijetí instance tohoto typu signály, že vaše komponenta má v úmyslu převzít vlastnictví této instance. Vaše komponenta bude zodpovědná za správnou likvidaci podle pravidla č. 7.

Každá komponenta, která převádí vlastnictví IMemoryOwner<T> instance na jinou komponentu, by už po dokončení volání metody neměla tuto instanci používat.

Důležité

Pokud konstruktor přijímá IMemoryOwner<T> jako parametr, měl by jeho typ implementovat IDisposablea vaše Dispose metoda by měla volat Dispose objekt IMemoryOwner<T> .

Pravidlo č. 9: Pokud zabalíte synchronní metodu p/invoke, vaše rozhraní API by mělo jako parametr přijmout SpanT<>.

Podle pravidla č. 1 je obecně správný typ, Span<T> který se má použít pro synchronní rozhraní API. Instance můžete připnout Span<T> pomocí klíčového slova, jako v následujícím příkladu fixed .

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

V předchozím příkladu může mít hodnotu null, pbData pokud je například vstupní rozsah prázdný. Pokud exportovaná metoda naprosto vyžaduje, aby pbData byla nenulová, i když cbData je 0, je možné metodu implementovat následujícím způsobem:

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

Pravidlo č. 10: Pokud zabalíte asynchronní metodu p/invoke, mělo by vaše rozhraní API přijmout funkci MemoryT<> jako parametr.

Vzhledem k tomu, že nemůžete použít fixed klíčové slovo napříč asynchronními operacemi, použijete Memory<T>.Pin metodu k připnutí Memory<T> instancí bez ohledu na druh souvislé paměti, kterou instance představuje. Následující příklad ukazuje, jak pomocí tohoto rozhraní API provést asynchronní volání p/invoke.

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

Viz také