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. Buffer
a 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 using
is 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 aMain
metódus a puffer tulajdonosa.A
WriteInt32ToBuffer
metódusok nyilvánosDisplayBufferToConsole
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 ahelyettvoid
, hogy a metódus következő implementációját követveLog
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 Dispose
IMemoryOwner<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
Visszajelzés
https://aka.ms/ContentUserFeedback.
Hamarosan elérhető: 2024-ben fokozatosan kivezetjük a GitHub-problémákat a tartalom visszajelzési mechanizmusaként, és lecseréljük egy új visszajelzési rendszerre. További információ:Visszajelzés küldése és megtekintése a következőhöz: