Pokyny pro využití paměti<T a rozsahu<T>>
.NET obsahuje řadu typů, které představují libovolnou souvislou oblast paměti. Span<T> a ReadOnlySpan<T> 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, nejsou vhodné pro scénáře, jako jsou asynchronní volání metod. Chcete-li tento problém vyřešit, .NET 2.1 přidal některé další typy, včetně Memory<T>, ReadOnlyMemory<T>, IMemoryOwner<T>a MemoryPool<T>. Memory<T> Podobně jako Span<T>a související typy mohou být podporovány spravovanou i nespravovanou pamětí. Na rozdíl od Span<T>, Memory<T> může být uložen na spravované haldě.
Oba Span<T> a Memory<T> jsou obálky přes 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 mohly být efektivně předány komponentám v kanálu, které je mohou zpracovat a volitelně upravit vyrovnávací paměť. Vzhledem k tomu, že Memory<T> ke souvisejícím typům se dá přistupovat více komponentami nebo více vlákny, je důležité postupovat podle některých standardních pokynů k používání a vytvořit robustní kód.
Vlastníci, spotřebitelé a správa životnosti
Vyrovnávací paměti je možné předávat mezi rozhraními API a někdy se k nim dá přistupovat z více vláken, takže mějte na paměti, jak se spravuje životnost vyrovnávací paměti. 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 rovněž převést; Component-A může relinquish řízení vyrovnávací paměti na Component-B, v němž komponenta-A již nemusí používat vyrovnávací paměť, a Component-B se stane zodpovědný za zničení vyrovnávací paměti, když už se nepoužívá.
Consumption. Příjemce instance vyrovnávací paměti může instanci vyrovnávací paměti používat č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 jejím vlastníkem. Proto je zodpovědný za zničení vyrovnávací paměti, Main
když se už nepoužívá. Pseudokód to ilustruje voláním Destroy
metody do vyrovnávací paměti. Memory<T>(Ani Span<T> ve skutečnosti nemá metoduDestroy
. Skutečné příklady kódu uvidíte dále v tomto článku.)
Vyrovnávací paměť má dva konzumenty a WriteInt32ToBuffer
DisplayBufferToConsole
. Najednou existuje pouze jeden příjemce (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é mohou upravovat obsah vyrovnávací paměti, stejně jako WriteInt32ToBuffer
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 začátkem volání metody a časem, kdy metoda vrátí. DisplayBufferToConsole
Podobně má zapůjčení vyrovnávací paměti při jeho provádění a zapůjčení se uvolní při odvíjení metody. (Pro správu zapůjčení neexistuje žádné rozhraní API; "zapůjčení" je koncepční záležitost.)
Paměť<T> a model vlastníka/příjemce
Jako poznámky k části Vlastníci, příjemci a správa životnosti má vyrovnávací paměť vždy vlastníka. .NET 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 (autora) 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.
Pomocí System.Buffers.IMemoryOwner<T> rozhraní můžete explicitně spravovat vlastnictví vyrovnávací paměti. IMemoryOwner<T> podporuje oba modely vlastnictví. Součást, která má IMemoryOwner<T> 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
{
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}'");
}
Tento příklad můžeme také napsat pomocí using
příkazu:
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}'");
}
V tomto kódu:
Metoda
Main
obsahuje odkaz na IMemoryOwner<T> instanci, takžeMain
metoda je vlastníkem vyrovnávací paměti.Metody
WriteInt32ToBuffer
aDisplayBufferToConsole
metody se přijímají Memory<T> jako veřejné rozhraní API. Jsou tedy spotřebiteli vyrovnávací paměti. Tyto metody spotřebovávají vyrovnávací paměť po jednom.
I když je metoda WriteInt32ToBuffer
určená k zápisu hodnoty do vyrovnávací paměti, DisplayBufferToConsole
metoda není určena. 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í readOnlySpan<T> nebo ReadOnlyMemory<T> , pokud by vyrovnávací paměť měla být jen pro čtení.
Instance paměti<T> bez vlastníka
Můžete vytvořit Memory<T> instanci 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[]
jak je znázorněno 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: ");
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}'");
}
Metoda, která původně vytvoří Memory<T> instanci, je implicitní vlastník vyrovnávací paměti. Vlastnictví nelze přenést do žádné jiné součásti, protože neexistuje žádná IMemoryOwner<T> instance pro usnadnění přenosu. (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 pro používání
Vzhledem k tomu, že blok paměti je vlastněný, ale má být předán více komponentám, některé z nich mohou pracovat s konkrétním blokem paměti současně, je důležité stanovit pokyny pro použití obou Memory<T> a Span<T>. Pokyny jsou nezbytné, protože komponenta může:
Po jeho vydání si zachovejte odkaz na blok paměti.
Operace s vyrovnávací pamětí ve stejnou dobu, kdy na ní pracuje jiná komponenta, v procesu poškození dat v vyrovnávací paměti.
I když přidělovaná sada Span<T> prostředků optimalizuje výkon a dává Span<T> upřednostňovaný typ pro provoz na paměťovém bloku, je to také předmětem Span<T> některých hlavních omezení. Je důležité vědět, kdy a kdy použít Span<T>Memory<T>.
Níže jsou uvedená 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 není uvedeno jinak.
Pravidlo č. 1: Pro synchronní rozhraní API použijte jako parametr Span<T> místo paměti<T> , pokud je to možné.
Span<T> je všestrannější než Memory<T> a může představovat širší škálu souvislých vyrovnávacích pamětí. Span<T> nabízí lepší výkon než Memory<T>. Nakonec můžete pomocí Memory<T>.Span vlastnosti převést Memory<T> instanci na Span<T>, ačkoli<Span T-to-Memory>< T> převod není možný. Takže pokud se volajícím stane, že mají Memory<T> instanci, budou moct přesto volat vaše metody s Span<T> parametry.
Použití parametru typu Span<T> místo typu Memory<T> vám také pomůže napsat správnou implementaci metody s využitím. Automaticky získáte kontroly doby kompilace, abyste se ujistili, že se nepokoušíte získat 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í při synchronním použití Memory<T> .
Pravidlo č. 2: Pokud má být vyrovnávací paměť jen pro čtení, použijte readOnlySpan<T> nebo ReadOnlyMemory<T> .
V předchozích příkladech metoda DisplayBufferToConsole
č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 toto pravidlo a pravidlo č. 1 zkombinujeme, 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é imaginovat: T[]
, úložiště přidělené stackalloc atd. Můžete dokonce předat String přímo do něj! Další informace najdete v tématu o problému GitHubu dotnet/docs #25551.
Pravidlo č. 3: Pokud vaše metoda přijímá Memory<T> a vrací void
, nesmíte po vrácení metody použít instanci Memory<T> .
To souvisí s konceptem "zapůjčení", který jsme zmínili dříve. Zapůjčení metody void-returning v Memory<T> instanci začíná při zadání metody a končí při 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)
{
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;
}
}
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.
Ale představte si, že Log
má tuto implementaci.
// !!! 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
může ztlumit vyrovnávací paměť při Log
pokusu o čtení z ní, což může vést k poškození dat.
Existuje několik způsobů, jak to vyřešit:
Metoda
Log
může místo Taskvoid
, protože následující implementaceLog
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 následujícím způsobem:// 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 paměť<T> a vrátí úlohu, nesmíte použít instanci Memory<T> po přechodu úlohy do stavu terminálu.
Toto je pouze asynchronní varianta pravidla č. 3. Metodu Log
z předchozího příkladu lze zapsat následujícím způsobem, aby vyhovovala tomuto pravidlu:
// 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();
});
}
V této části "stav terminálu" znamená, že úloha přejde do dokončeného, chybného nebo zrušeného stavu. Jinými slovy, "stav terminálu" znamená "cokoli, co by způsobilo vyvolání nebo pokračování v provádění".
Tyto pokyny platí pro metody, které vracejí Task, Task<TResult>, ValueTask<TResult>nebo jakýkoli podobný typ.
Pravidlo č. 5: Pokud váš konstruktor přijímá memory<T> jako parametr, metody instance v konstruovaném objektu se předpokládají jako příjemci instance Memory<T> instance.
Představte si 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 ve vrácené hodnotě 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 T-typed memory<> (nebo ekvivalentní metodu instance) vašeho typu, metody instance na tomto objektu se předpokládají jako příjemci instance Memory<T> instance.
Toto je opravdu jen varianta pravidla č. 5. Toto pravidlo existuje, protože setter vlastností nebo ekvivalentní metody předpokládají zachycení 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 IMemoryOwner<T> , musíte ho v určitém okamžiku odstranit 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 Dispose
IMemoryOwner<T>. Případně může vlastník převést vlastnictví IMemoryOwner<T> instance na jinou komponentu, v tomto 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 likvidaci instance.
Pravidlo č. 8: Pokud máte ve svém rozhraní API parametr IMemoryOwner<T> , přijímáte vlastnictví této instance.
Přijetí instance tohoto typu signalizuje, že vaše komponenta hodlá převzít vlastnictví této instance. Vaše komponenta je zodpovědná za správnou likvidaci podle pravidla č. 7.
Každá komponenta, která převádí vlastnictví IMemoryOwner<T> instance na jinou komponentu, by již neměla tuto instanci používat po dokončení volání metody.
Důležité
Pokud konstruktor přijímá IMemoryOwner<T> jako parametr, jeho typ by měl 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 Span<T> .
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 fixed
slova, jako v následujícím příkladu.
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 být null, pbData
pokud je například vstupní rozsah prázdný. Pokud exportovaná metoda absolutně vyžaduje, aby pbData
byla nenulová, i když cbData
je 0, lze metodu implementovat takto:
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, vaše rozhraní API by mělo přijmout paměť<T> jako parametr.
Vzhledem k tomu, že nemůžete použít fixed
klíčové slovo napříč asynchronními operacemi, použijete metodu Memory<T>.Pin pro 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;
}