Poznámka:
Přístup k této stránce vyžaduje autorizaci. Můžete se zkusit přihlásit nebo změnit adresáře.
Přístup k této stránce vyžaduje autorizaci. Můžete zkusit změnit adresáře.
.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ěť. Protože tyto typy mohou být uloženy pouze na stacku, 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>. Span<T> Podobně jako Memory<T>a související typy mohou být podporovány spravovanou i nespravovanou pamětí. Na rozdíl od Span<T> může být Memory<T> uložen na spravované haldě.
Oba Span<T> a Memory<T> jsou wrappers přes buffery strukturovaných dat, které lze použít v pipelinách. To znamená, že jsou navrženy tak, aby některá nebo všechna data mohla být efektivně předána komponentám v datovém toku, které je mohou zpracovat a volitelně upravit vyrovnávací paměť. Vzhledem k tomu, že k Memory<T> a souvisejícím typům může přistupovat více komponent nebo vláken, je důležité dodržovat standardní pokyny pro 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 předat kontrolu vyrovnávací paměti na Component-B, v němž Component-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á.
Spotřeba. 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í. Nájemní smlouva je doba, po kterou je určité komponentě povoleno být spotřebitelem 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 tím se stává jejím vlastníkem. Proto Main odpovídá za zničení vyrovnávací paměti, 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, WriteInt32ToBuffer a 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á kontrolu nad vyrovnávací pamětí mezi začátkem volání metody a časem, kdy metoda vrátí.
DisplayBufferToConsole Podobně má pronájem vyrovnávací paměti během jeho provedení a pronájem se uvolní při dokončení 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/spotřebitele
Jak je uvedeno v části Vlastníci, příjemci a správa životnosti, vyrovnávací paměť má vždy vlastníka. .NET podporuje dva modely vlastnictví:
- Model, který podporuje jedno vlastnictví. Vyrovnávací paměť má jednoho vlastníka po celou dobu životnosti.
- 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á vlastní IMemoryOwner<T> odkaz, má 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ůžete 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
Mainobsahuje odkaz na IMemoryOwner<T> instanci, takžeMainmetoda je vlastníkem vyrovnávací paměti. - Metody
WriteInt32ToBufferaDisplayBufferToConsolepř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, metoda DisplayBufferToConsole není k tomuto účelu určena. Aby to bylo možné odrážet, mohlo by to přijmout argument typu ReadOnlyMemory<T>. Další informace o ReadOnlyMemory<T>, viz pravidlo č. 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:
- Přímé volání jednoho z Memory<T> konstruktorů, předání
T[], jak je uvedeno v následujícím příkladu. - Volání rozšiřující metody String.AsMemory k vytvoření instance
ReadOnlyMemory<char>.
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řila instanci Memory<T>, je implicitním vlastníkem 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 vyrovnávací paměť vlastní garbage collector modulu runtime a všechny metody ji pouze využívají.)
Pokyny k 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 uvolnění vlastníkem si zachovejte odkaz na blok paměti.
- Operovat na vyrovnávací paměti zároveň s tím, že na ní pracuje jiná komponenta, což vede k poškození dat ve vyrovnávací paměti.
I když povaha alokována na zásobníku Span<T> optimalizuje výkon a činí z Span<T> preferovaný typ pro práci s paměťovým blokem, také podléhá Span<T> některým hlavním omezením. 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
Span<T>místoMemory<T>parametru, pokud je to možné. -
Pravidlo č. 2: Použijte
ReadOnlySpan<T>neboReadOnlyMemory<T>pokud má být vyrovnávací paměť určená jen pro čtení -
Pravidlo č. 3: Pokud vaše metoda přijímá
Memory<T>a vracívoid, nesmíte použítMemory<T>instanci po vrácení metody -
Pravidlo č. 4: Pokud vaše metoda přijme
Memory<T>a vrátí úlohu, nesmíte použítMemory<T>instanci po přechodu úlohy do stavu terminálu. -
Pravidlo č. 5: Pokud konstruktor přijímá
Memory<T>jako parametr, metody instance v konstruovaném objektu se předpokládají jako příjemciMemory<T>instance. - Pravidlo č. 6: Pokud máte u typu nastavitelnou vlastnost typu
Memory<T>(nebo ekvivalentní metodu instance), předpokládá se, že instance metody na tomto objektu využívají instanciMemory<T>. -
Pravidlo č. 7: Pokud máte
IMemoryOwner<T>odkaz, musíte ho v určitém okamžiku likvidovat nebo převést jeho vlastnictví (ale ne obojí). -
Pravidlo č. 8: Pokud máte
IMemoryOwner<T>v povrchu rozhraní API parametr, přijímáte vlastnictví této instance. -
Pravidlo č. 9: Pokud zabalíte synchronní metodu P/Invoke, mělo by vaše rozhraní API přijmout
Span<T>jako parametr. -
Pravidlo č. 10: Pokud zabalíte asynchronní metodu p/invoke, vaše rozhraní API by mělo přijmout
Memory<T>jako parametr.
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 paměťových oblastí. 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 při kompilaci, abyste se ujistili, že se nepokoušíte získat přístup k vyrovnávací paměti mimo platnost vaší metody (více o tom později).
Někdy budete muset použít parametr Memory<T> místo parametru Span<T>, 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 spojené se synchronním použitím 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 zkombinujete toto pravidlo a pravidlo č. 1, můžeme to udělat 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 ve smyčce na základě vstupu z konzole.
// <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);
// }
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 Log porušuje své zapůjčení, protože se stále pokouší použít instanci Memory<T> na pozadí poté, co původní metoda byla vrácena. Metoda Main může modifikovat vyrovnávací paměť, zatímco Log se pokouší číst z ní, což může vést k poškození dat.
Existuje několik způsobů, jak to vyřešit:
Metoda
Logmůže vrátit Task místovoid, jak ukazuje následující implementace metodyLog.// 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(); }); }Loglze 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 konstruktor přijímá paměť<T> jako parametr, metody instance v konstruovaném objektu se předpokládají jako příjemci instance Memory<T> .
Podívejte se na 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 u svého typu konfigurovatelnou vlastnost typu Memory<T> (nebo ekvivalentní metodu instance), předpokládá se, že instance metod tohoto objektu budou konzumenti instance Memory<T>.
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í).
Vzhledem k tomu, že instance Memory<T> může být zajištěna buď spravovanou nebo nespravovanou pamětí, musí vlastník po dokončení práce na instanci Dispose zavolat na IMemoryOwner<T>Memory<T>. Případně může vlastník převést vlastnictví IMemoryOwner<T> instance na jinou komponentu, přičemž získaná komponenta bude mít na starosti zavolání Dispose ve vhodnou dobu (více o tom později).
Selhání zavolat metodu Dispose na instanci IMemoryOwner<T> může vést k únikům nespravované paměti nebo jinému zhoršení výkonu.
Toto pravidlo platí také pro kód, který volá tovární metody, 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 váš konstruktor přijímá IMemoryOwner<T> jako parametr, jeho typ by měl implementovat IDisposable, a vaše metoda Dispose by měla volat Dispose na objektu 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 Span<T> obecně správný typ, který se má použít pro synchronní rozhraní API. Můžete připnout instance Span<T> pomocí klíčového slova fixed, jak ukazuje následující příklad.
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;
}