Nota
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare ad accedere o modificare le directory.
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare a modificare le directory.
.NET include diversi tipi che rappresentano un'area contigua arbitraria di memoria. Span<T> e ReadOnlySpan<T> sono buffer di memoria leggeri che incapsulano riferimenti alla memoria gestita o non gestita. Poiché questi tipi possono essere archiviati solo nello stack, non sono adatti per scenari come le chiamate asincrone ai metodi. Per risolvere questo problema, .NET 2.1 ha aggiunto alcuni tipi aggiuntivi, tra cui Memory<T>, ReadOnlyMemory<T>IMemoryOwner<T>, e MemoryPool<T>. Come Span<T>e Memory<T> i tipi correlati possono essere supportati sia dalla memoria gestita che dalla memoria non gestita. A differenza di Span<T>, Memory<T> può essere archiviato nell'heap gestito.
Sia Span<T> sia Memory<T> sono wrapper per buffer di dati strutturati che possono essere utilizzati in pipeline. Ovvero, sono progettati in modo che alcuni o tutti i dati possano essere passati in modo efficiente ai componenti della pipeline, che possono elaborarli e, facoltativamente, modificare il buffer. Poiché Memory<T> e i relativi tipi possono essere accessibili da più componenti o da più thread, è importante seguire alcune linee guida di utilizzo standard per produrre codice affidabile.
Proprietari, consumatori e gestione della vita utile
I buffer possono essere passati tra le API e a volte possono essere accessibili da più thread, quindi tenere presente come viene gestita la durata di un buffer. Esistono tre concetti fondamentali:
Titolarità. Il proprietario di un'istanza del buffer è responsabile della gestione della durata, inclusa l'eliminazione del buffer quando non è più in uso. Tutti i buffer hanno un singolo proprietario. In genere il proprietario è il componente che ha creato il buffer o che ha ricevuto il buffer da una fabbrica. La proprietà può anche essere trasferita; Component-A può rinunciare al controllo del buffer in Component-B, in cui il componente A non può più usare il buffer e Component-B diventa responsabile dell'eliminazione del buffer quando non è più in uso.
Consumo. Il consumatore di un'istanza di buffer può utilizzare quest'istanza leggendo da essa ed eventualmente scrivendo su di essa. I buffer possono avere un consumer alla volta, a meno che non venga fornito un meccanismo di sincronizzazione esterno. Il consumer attivo di un buffer non è necessariamente il proprietario del buffer.
Contratto di locazione. La durata del contratto è il periodo di tempo in cui un specifico componente può essere l'utilizzatore del buffer.
Nell'esempio di pseudo-codice seguente vengono illustrati questi tre concetti.
Buffer
nello pseudo-codice rappresenta un buffer Memory<T> o Span<T> di tipo Char. Il Main
metodo crea un'istanza del buffer, chiama il WriteInt32ToBuffer
metodo per scrivere la rappresentazione di stringa di un intero nel buffer e quindi chiama il DisplayBufferToConsole
metodo per visualizzare il valore del buffer.
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();
}
}
}
Il Main
metodo crea il buffer e quindi è il relativo proprietario. Pertanto, Main
è responsabile dell'eliminazione del buffer quando non è più in uso. Lo pseudocodice illustra questa operazione chiamando un Destroy
metodo nel buffer. (Né Memory<T> né Span<T> in realtà ha un Destroy
metodo. Più avanti in questo articolo verranno visualizzati esempi di codice effettivi.
Il buffer ha due consumatori, WriteInt32ToBuffer
e DisplayBufferToConsole
. Esiste un solo consumer alla volta (primo WriteInt32ToBuffer
, quindi DisplayBufferToConsole
) e nessuno dei consumer è proprietario del buffer. Si noti anche che "consumer" in questo contesto non implica una visualizzazione di sola lettura del buffer; I consumer possono modificare il contenuto del buffer, come fa WriteInt32ToBuffer
, se viene fornita una visualizzazione di lettura/scrittura del buffer.
Il WriteInt32ToBuffer
metodo ha un controllo su (può utilizzare) il buffer dall'inizio della chiamata fino al momento in cui il metodo restituisce. Analogamente, DisplayBufferToConsole
ha un lease sul buffer durante l'esecuzione e il lease viene rilasciato quando il metodo viene rimosso. Non esiste alcuna API per la gestione dei lease. Un "lease" è una questione concettuale.
Memoria<T> e modello proprietario/consumatore
Come indicato nella sezione Proprietari, consumatori e gestione del ciclo di vita, un buffer ha sempre un proprietario. .NET supporta due modelli di proprietà:
- Modello che supporta la singola proprietà. Un buffer ha un singolo proprietario per l'intera durata.
- Modello che supporta il trasferimento della proprietà. La proprietà di un buffer può essere trasferita dal proprietario originale (creatore) a un altro componente, che diventa quindi responsabile della gestione della durata del buffer. Tale proprietario può a sua volta trasferire la proprietà a un altro componente e così via.
Usare l'interfaccia System.Buffers.IMemoryOwner<T> per gestire in modo esplicito la proprietà di un buffer. IMemoryOwner<T> supporta entrambi i modelli di proprietà. Il componente che dispone di un IMemoryOwner<T> riferimento è proprietario del buffer. Nell'esempio seguente viene utilizzata un'istanza IMemoryOwner<T> di per riflettere la proprietà di un Memory<T> buffer.
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}'");
}
È possibile scrivere anche questo esempio con l'istruzione 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
{
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}'");
}
In questo codice:
- Il
Main
metodo contiene il riferimento all'istanza IMemoryOwner<T> , quindi ilMain
metodo è il proprietario del buffer. - I
WriteInt32ToBuffer
metodi eDisplayBufferToConsole
accettano Memory<T> come API pubblica. Pertanto, sono consumatori del buffer. Questi metodi utilizzano il buffer uno alla volta.
Anche se il WriteInt32ToBuffer
metodo è destinato a scrivere un valore nel buffer, il DisplayBufferToConsole
metodo non è destinato a . Per riflettere questo problema, potrebbe aver accettato un argomento di tipo ReadOnlyMemory<T>. Per altre informazioni su ReadOnlyMemory<T>, vedere Regola 2: Usare ReadOnlySpan<T> o ReadOnlyMemory<T> se il buffer deve essere di sola lettura.
"Memoria <"ownerless"> delle istanze T"
È possibile creare un'istanza Memory<T> senza usare IMemoryOwner<T>. In questo caso, la proprietà del buffer è implicita anziché esplicita e solo il modello a proprietario singolo è supportato. A tale scopo, è possibile:
- Chiamando direttamente uno dei Memory<T> costruttori, passando un
T[]
, come illustrato nell'esempio seguente. - Chiamata del metodo di estensione String.AsMemory per produrre un'istanza
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}'");
}
Il metodo che inizialmente crea l'istanza Memory<T> è il proprietario implicito del buffer. La proprietà non può essere trasferita ad altri componenti perché non esiste alcuna IMemoryOwner<T> istanza per facilitare il trasferimento. In alternativa, è anche possibile immaginare che il Garbage Collector del runtime sia proprietario del buffer e che tutti i metodi usano semplicemente il buffer.
Linee guida per l'utilizzo
Poiché un blocco di memoria è di proprietà, ma deve essere passato a più componenti, alcuni dei quali possono funzionare contemporaneamente su un determinato blocco di memoria, è importante stabilire linee guida per l'uso Memory<T> di e Span<T>. Le linee guida sono necessarie perché è possibile che un componente:
- Mantenere un riferimento a un blocco di memoria dopo che il proprietario l'ha rilasciato.
- Operare su un buffer contemporaneamente al funzionamento di un altro componente, nel processo che danneggia i dati nel buffer.
Anche se la natura di allocazione nello stack di Span<T> ottimizza le prestazioni e rende Span<T> il tipo preferito per l'uso in un blocco di memoria, ciò sottopone anche Span<T> ad alcune restrizioni principali. È importante sapere quando usare un Span<T> e quando usare un Memory<T>.
Di seguito sono riportati i suggerimenti per l'uso Memory<T> corretto e i relativi tipi correlati. Le linee guida applicabili a Memory<T> e Span<T> si applicano anche a ReadOnlyMemory<T> e ReadOnlySpan<T>, se non diversamente specificato.
-
Regola 1: Per un'API sincrona, usare
Span<T>
anzichéMemory<T>
come parametro, se possibile -
Regola 2: Usare
ReadOnlySpan<T>
oReadOnlyMemory<T>
se il buffer deve essere di sola lettura -
Regola 3: Se il metodo accetta
Memory<T>
e restituiscevoid
, non si deve usare l'istanzaMemory<T>
dopo che il metodo restituisce -
Regola n. 4: Se il tuo metodo accetta un
Memory<T>
e restituisce un Task, non devi usare l'istanzaMemory<T>
dopo che il Task è passato a uno stato terminale -
Regola 5: se il costruttore accetta
Memory<T>
come parametro, si presuppone che i metodi di istanza dell'oggetto costruito siano consumer dell'istanzaMemory<T>
- Regola n°6: se si dispone di una proprietà di tipo
Memory<T>
impostabile (o di un metodo di istanza equivalente) nel proprio tipo, si presume che i metodi di istanza su quell'oggetto siano utilizzatori dell'istanzaMemory<T>
. -
Regola 7: Se si dispone di un
IMemoryOwner<T>
riferimento, è necessario eliminarla o trasferirne la proprietà (ma non entrambe) -
Regola 8: Se si dispone di un
IMemoryOwner<T>
parametro nell'area API, si accetta la proprietà di tale istanza -
Regola 9: se si esegue il wrapping di un metodo P/Invoke sincrono, l'API deve accettare
Span<T>
come parametro -
Regola 10: se si esegue il wrapping di un metodo p/invoke asincrono, l'API deve accettare
Memory<T>
come parametro
Regola 1: Per un'API sincrona, usare Span<T> anziché Memory<T> come parametro, se possibile
Span<T> è più versatile di Memory<T> e può rappresentare una varietà più ampia di buffer di memoria contigui. Span<T> offre anche prestazioni migliori rispetto a Memory<T>. Infine, è possibile utilizzare la proprietà Memory<T>.Span per convertire un'istanza Memory<T> in un Span<T>, anche se la conversione Span<T>-to-Memory<T> non è possibile. Pertanto, se i chiamanti hanno un'istanza Memory<T>, potranno comunque chiamare i tuoi metodi usando Span<T> come parametri.
L'uso di un parametro di tipo Span<T> anziché di tipo Memory<T> consente anche di scrivere un'implementazione corretta del metodo di utilizzo. Si otterranno automaticamente controlli in fase di compilazione per assicurarsi di non tentare di accedere al buffer oltre il lease del metodo (più avanti).
In alcuni casi, è necessario usare un Memory<T> parametro anziché un Span<T> parametro, anche se si è completamente sincroni. Ad esempio, un'API da cui si dipende accetta solo Memory<T> argomenti. Ciò va bene, ma tenere presente i compromessi coinvolti quando si usa Memory<T> in modo sincrono.
Regola 2: Usare ReadOnlySpan<T> o ReadOnlyMemory<T> se il buffer deve essere di sola lettura
Negli esempi precedenti il DisplayBufferToConsole
metodo legge solo dal buffer e non modifica il contenuto del buffer. La firma del metodo deve essere modificata nel modo seguente.
void DisplayBufferToConsole(ReadOnlyMemory<char> buffer);
Infatti, se si combina questa regola e regola 1, è possibile migliorare e riscrivere la firma del metodo come indicato di seguito:
void DisplayBufferToConsole(ReadOnlySpan<char> buffer);
Il DisplayBufferToConsole
metodo ora funziona praticamente con ogni tipo di buffer immaginabile: T[]
, archiviazione allocata con stackalloc e così via. Puoi anche passare un String direttamente al suo interno! Per altre informazioni, vedere Problema di GitHub dotnet/docs #25551.
Regola #3: Se il metododo accetta Memory<T> e restituisce void
, non devi usare l'istanza Memory<T> dopo che il metodo ha restituito.
Questo si riferisce al concetto di "lease" menzionato in precedenza. Il lease di un metodo che restituisce void sull'istanza Memory<T> inizia quando viene immesso il metodo e termina quando il metodo viene chiuso. Si consideri il seguente esempio, che chiama Log
in un ciclo in base all'input della console.
// <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);
// }
Se Log
è un metodo completamente sincrono, questo codice si comporterà come previsto perché esiste un solo consumer attivo dell'istanza di memoria in qualsiasi momento.
Si supponga invece che Log
abbia questa implementazione.
// !!! 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);
});
}
In questa implementazione viola Log
il lease perché tenta comunque di usare l'istanza Memory<T> in background dopo la restituzione del metodo originale. Il Main
metodo potrebbe modificare il buffer durante Log
i tentativi di lettura da esso, che potrebbe causare un danneggiamento dei dati.
Esistono diversi modi per risolvere questo problema:
Il
Log
metodo può restituire invece Task divoid
, come l'implementazione seguente delLog
metodo .// 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
può invece essere implementato come segue:// 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(); }); }
Regola 4: Se il metodo accetta una memoria<T> e restituisce un'attività, non è necessario usare l'istanza T< di memoria>dopo la transizione dell'attività a uno stato terminale
Si tratta solo della variante asincrona della regola n. 3. Il Log
metodo dell'esempio precedente può essere scritto come segue per rispettare questa regola:
// 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();
});
}
In questo caso, "stato terminale" significa che l'attività passa a uno stato completato, con errori o annullato. In altre parole, "stato terminale" significa "qualsiasi elemento che provocherebbe l'attesa di generare o continuare l'esecuzione".
Queste linee guida si applicano ai metodi che restituiscono Task, Task<TResult>, ValueTask<TResult>o qualsiasi tipo simile.
Regola 5: Se il costruttore accetta Memory<T> come parametro, si presuppone che i metodi di istanza dell'oggetto costruito siano consumer dell'istanza memory<T>
Si consideri l'esempio seguente:
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);
}
}
In questo caso, il costruttore OddValueExtractor
accetta una ReadOnlyMemory<int>
come parametro del costruttore, quindi il costruttore stesso è un utilizzatore dell'istanza ReadOnlyMemory<int>
e tutti i metodi di istanza del valore restituito sono anche utilizzatori dell'istanza originale ReadOnlyMemory<int>
. Ciò significa che TryReadNextOddValue
usa l'istanza ReadOnlyMemory<int>
, anche se l'istanza non viene passata direttamente al TryReadNextOddValue
metodo .
Regola 6: se si dispone di una proprietà T tipizzata memoria<> impostabile (o un metodo di istanza equivalente) nel tipo, si presuppone che i metodi di istanza per tale oggetto siano consumer dell'istanza T< di memoria>
Si tratta in realtà di una variante della regola n. 5. Questa regola esiste perché si presuppone che i setter di proprietà o i metodi equivalenti acquisiscano e mantengono gli input, quindi i metodi di istanza sullo stesso oggetto possono utilizzare lo stato acquisito.
L'esempio seguente attiva questa regola:
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;
}
Regola 7: Se si dispone di un riferimento IMemoryOwner<T> , è necessario eliminarla o trasferirne la proprietà (ma non entrambe)
Poiché un'istanza Memory<T> può essere supportata da memoria gestita o non gestita, il proprietario deve chiamare Dispose
IMemoryOwner<T> quando il lavoro eseguito sull'istanza Memory<T> è completato. In alternativa, il proprietario può trasferire la proprietà dell'istanza IMemoryOwner<T> a un componente diverso, a questo punto il componente di acquisizione diventa responsabile della chiamata Dispose
al momento appropriato (più avanti).
La mancata chiamata del metodo Dispose
su un'istanza IMemoryOwner<T> può causare perdite di memoria non gestite o altri problemi di prestazioni.
Questa regola si applica anche al codice che chiama metodi factory come MemoryPool<T>.Rent. Il chiamante diventa il proprietario del valore restituito IMemoryOwner<T> ed è responsabile dell'eliminazione dell'istanza al termine dell'uso.
Regola 8: se si dispone di un parametro IMemoryOwner<T> nell'area API, si accetta la proprietà di tale istanza
L'accettazione di un'istanza di questo tipo segnala che il componente intende assumere la proprietà di questa istanza. Il tuo componente diventa responsabile della corretta eliminazione conformemente alla Regola #7.
Qualsiasi componente che trasferisce la proprietà dell'istanza IMemoryOwner<T> a un componente diverso non deve più usare tale istanza al termine della chiamata al metodo.
Importante
Se il costruttore accetta IMemoryOwner<T> come parametro, il relativo tipo deve implementare IDisposablee il Dispose metodo deve chiamare Dispose
sull'oggetto IMemoryOwner<T> .
Regola 9: se si esegue il wrapping di un metodo p/invoke sincrono, l'API deve accettare Span<T> come parametro
In base alla regola 1, Span<T> è in genere il tipo corretto da usare per le API sincrone. È possibile fissare Span<T> istanze tramite la parola chiave fixed
, come nell'esempio seguente.
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;
}
}
Nell'esempio precedente, pbData
può essere Null se, ad esempio, l'intervallo di input è vuoto. Se il metodo esportato richiede assolutamente che pbData
non sia Null, anche se cbData
è 0, il metodo può essere implementato come segue:
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;
}
}
Regola 10: se si esegue il wrapping di un metodo p/invoke asincrono, l'API deve accettare Memory<T> come parametro
Poiché non è possibile usare la fixed
parola chiave nelle operazioni asincrone, è possibile usare il Memory<T>.Pin metodo per aggiungere Memory<T> istanze, indipendentemente dal tipo di memoria contigua rappresentata dall'istanza. L'esempio seguente illustra come usare questa API per eseguire una chiamata p/invoke asincrona.
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;
}