Observação
O acesso a essa página exige autorização. Você pode tentar entrar ou alterar diretórios.
O acesso a essa página exige autorização. Você pode tentar alterar os diretórios.
O .NET inclui vários tipos que representam uma região contígua arbitrária de memória. Span<T> e ReadOnlySpan<T> são buffers de memória leves que encapsulam referências à memória gerenciada ou não gerenciada. Como esses tipos só podem ser armazenados na pilha, eles são inadequados para cenários como chamadas de método assíncronas. Para resolver esse problema, o .NET 2.1 adicionou alguns tipos adicionais, incluindo Memory<T>, ReadOnlyMemory<T>e IMemoryOwner<T>MemoryPool<T>. Como Span<T>, Memory<T> e seus tipos relacionados podem ser apoiados por memória gerenciada e não gerenciada. Ao contrário de Span<T>, Memory<T> pode ser armazenado no heap gerenciado.
Tanto Span<T> como Memory<T> são wrappers sobre buffers de dados estruturados que podem ser usados em pipelines. Ou seja, eles são projetados para que alguns ou todos os dados possam ser passados com eficiência para componentes no pipeline, o que pode processá-los e, opcionalmente, modificar o buffer. Como Memory<T> e seus tipos relacionados podem ser acessados por vários componentes ou por vários threads, é importante seguir algumas diretrizes de uso padrão para produzir código robusto.
Proprietários, consumidores e gerenciamento de tempo de vida
Os buffers podem ser passados entre APIs e, às vezes, podem ser acessados de vários threads, portanto, lembre-se de como o tempo de vida de um buffer é gerenciado. Há três conceitos principais:
Propriedade. O proprietário de uma instância de buffer é responsável pelo gerenciamento de tempo de vida, incluindo a destruição do buffer quando ele não está mais em uso. Todos os buffers têm um único proprietário. Geralmente, o proprietário é o componente que criou o buffer ou que recebeu o buffer de uma fábrica. A propriedade também pode ser transferida; O componente A pode abrir mão do controle do buffer para o Componente B, momento em que o Componente A pode não usar mais o buffer e o Componente B se torna responsável por destruir o buffer quando ele não estiver mais em uso.
Consumo. O consumidor de uma instância do buffer pode usar essa instância, lendo e possivelmente gravando nela. Os buffers podem ter um consumidor por vez, a menos que algum mecanismo de sincronização externo seja fornecido. O consumidor ativo de um buffer não é necessariamente o proprietário do buffer.
Concessão. A concessão é o período de tempo em que um componente específico pode ser o consumidor do buffer.
O exemplo de pseudocódigo a seguir ilustra esses três conceitos.
Buffer no pseudocódigo representa um Memory<T> ou Span<T> buffer do tipo Char. O método Main instancia o buffer, chama o método WriteInt32ToBuffer para gravar a representação de uma cadeia de caracteres de um inteiro no buffer e, em seguida, chama o método DisplayBufferToConsole para exibir o valor do 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();
}
}
}
O método Main cria o buffer e, portanto, ele é o respectivo proprietário. Portanto, Main é responsável por destruir o buffer quando ele não está mais em uso. O pseudocódigo ilustra isso chamando um Destroy método no buffer. (Nem Memory<T>Span<T> realmente tem um Destroy método. Você verá exemplos de código reais mais adiante neste artigo.)
O buffer tem dois consumidores WriteInt32ToBuffer e DisplayBufferToConsole. Há apenas um consumidor de cada vez (primeiro WriteInt32ToBuffer, depois DisplayBufferToConsole) e nenhum dos consumidores possui o buffer. Ainda, "consumidor" neste contexto não implica uma exibição somente leitura do buffer. Os consumidores podem modificar o conteúdo do buffer, assim como WriteInt32ToBuffer o faz, se for fornecida uma exibição de leitura/gravação do buffer.
O método WriteInt32ToBuffer tem uma concessão (pode consumir) o buffer entre o início da chamada do método e a hora em que o método é retornado. Da mesma maneira, DisplayBufferToConsole tem uma concessão no buffer enquanto ele está em execução, e a concessão é liberada quando o método é desenrolado. (Não há uma API para gerenciamento de concessão; uma "concessão" é uma questão conceitual.)
Memory<T> e o modelo proprietário/consumidor
Como observa a seção Proprietários, consumidores e gerenciamento de tempo de vida , um buffer sempre tem um proprietário. O .NET dá suporte a dois modelos de propriedade:
- Um modelo que dá suporte à propriedade única. Um buffer tem um único proprietário durante toda a sua vida útil.
- Um modelo que dá suporte à transferência de propriedade. A propriedade de um buffer pode ser transferida de seu proprietário original (seu criador) para outro componente, que se torna responsável pelo gerenciamento de tempo de vida do buffer. Esse proprietário pode, por sua vez, transferir a propriedade para outro componente e assim por diante.
Você usa a System.Buffers.IMemoryOwner<T> interface para gerenciar explicitamente a propriedade de um buffer. IMemoryOwner<T> dá suporte a ambos os modelos de propriedade. O componente que tem uma IMemoryOwner<T> referência é proprietário do buffer. O exemplo a seguir usa uma IMemoryOwner<T> instância para refletir a propriedade de um 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}'");
}
Você também pode escrever este exemplo com a declaraçãousing:
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}'");
}
Neste código:
- O
Mainmétodo mantém a referência à IMemoryOwner<T> instância, portanto, oMainmétodo é o proprietário do buffer. - Os
WriteInt32ToBuffermétodos e os métodosDisplayBufferToConsoleaceitam Memory<T> como uma API pública. Portanto, eles são consumidores do buffer. Esses métodos consomem o buffer um de cada vez.
Embora o método WriteInt32ToBuffer se destine a gravar um valor no buffer, o método DisplayBufferToConsole não tem essa intenção. Para refletir isso, ele poderia ter aceitado um argumento do tipo ReadOnlyMemory<T>. Para obter mais informações sobre ReadOnlyMemory<T>, consulte a Regra nº 2: Use ReadOnlySpan<T> ou ReadOnlyMemory<T> se o buffer deve ser somente leitura.
Instâncias de Memory<T> "sem proprietário"
Você pode criar uma Memory<T> instância sem usar IMemoryOwner<T>. Nesse caso, a propriedade do buffer é implícita em vez de explícita, e somente o modelo de proprietário único tem suporte. Você pode fazer isso:
- Chamar um dos construtores de Memory<T> diretamente, passando um
T[], assim como no exemplo a seguir. - Chamando o método de extensão String.AsMemory para produzir uma
ReadOnlyMemory<char>instância.
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}'");
}
O método que inicialmente cria a Memory<T> instância é o proprietário implícito do buffer. A propriedade não pode ser transferida para qualquer outro componente porque não há nenhuma IMemoryOwner<T> instância para facilitar a transferência. (Como alternativa, você também pode imaginar que o coletor de lixo do runtime possui o buffer e todos os métodos consomem apenas o buffer.)
Diretrizes de uso
Como um bloco de memória é de propriedade, mas se destina a ser passado para vários componentes, alguns dos quais podem operar em um bloco de memória específico simultaneamente, é importante estabelecer diretrizes para usar ambos Memory<T> e Span<T>. As diretrizes são necessárias porque é possível que um componente:
- Retenha uma referência a um bloco de memória depois que o respectivo proprietário o liberar.
- Opere em um buffer ao mesmo tempo em que outro componente está operando nele, corrompendo os dados no buffer.
Embora a natureza alocada na pilha do Span<T> otimize o desempenho e torne Span<T> o tipo preferencial para a operação em um bloco de memória, ele também submete Span<T> para algumas restrições importantes. É importante saber quando usar um Span<T> e quando usar Memory<T>.
Veja a seguir nossas recomendações para usar Memory<T> com êxito e seus tipos relacionados. As diretrizes que se aplicam a Memory<T> e Span<T> também se aplicam a ReadOnlyMemory<T> e ReadOnlySpan<T>, a menos que indicado de outra forma.
-
Regra nº 1: para uma API síncrona, use
Span<T>em vez deMemory<T>como um parâmetro, se possível -
Regra n° 2: Usar
ReadOnlySpan<T>ouReadOnlyMemory<T>se o buffer deve ser somente leitura -
Regra nº 3: se o método aceitar
Memory<T>e retornarvoid, você não deverá usar a instânciaMemory<T>após o método retornar -
Regra nº 4: se o seu método aceitar um
Memory<T>e retornar uma Tarefa, você não deverá usar a instânciaMemory<T>após a Tarefa transitar para um estado terminal - Regra nº 5: se o construtor aceitar
Memory<T>como um parâmetro, os métodos de instânciaMemory<T>no objeto construído serão considerados consumidores da instância - Regra nº 6: Se você tiver uma propriedade do tipo ajustável
Memory<T>(ou um método de instância equivalente) no seu tipo, os métodos de instância desse objeto são considerados como consumidores da instânciaMemory<T>. -
Regra n° 7: quando há uma referência de
IMemoryOwner<T>, será necessário descartá-la ou transferir a propriedade (mas não as duas coisas), em algum momento -
Regra nº 8: se você tiver um
IMemoryOwner<T>parâmetro em sua superfície de API, você está aceitando a propriedade dessa instância -
Regra nº 9: se você estiver encapsulando um método P/Invoke síncrono, sua API deverá aceitar
Span<T>como um parâmetro -
Regra nº 10: se você estiver encapsulando um método de p/invoke assíncrono, sua API deverá aceitar
Memory<T>como um parâmetro
Regra nº 1: para uma API síncrona, use Span<T> em vez de Memory<T> como um parâmetro, se possível
Span<T> é mais versátil do que Memory<T> e pode representar uma variedade maior de buffers de memória contíguos. Span<T> também oferece melhor desempenho do que Memory<T>. Por fim, você pode usar a propriedade Memory<T>.Span para converter uma instância Memory<T> em um Span<T>, embora a conversão de Span<T> para Memory<T> não seja possível. Portanto, se os chamadores tiverem uma instância de Memory<T>, eles poderão chamar seus métodos com os parâmetros Span<T> de qualquer maneira.
Usar um parâmetro de tipo Span<T> em vez de tipo Memory<T> também ajuda você a escrever uma implementação correta do método de consumo. Você obterá automaticamente verificações durante o tempo de compilação para garantir que não está tentando acessar o buffer além do limite do método (mais sobre isso posteriormente).
Às vezes, você precisará usar um Memory<T> parâmetro em vez de um Span<T> parâmetro, mesmo que seja totalmente síncrono. Talvez uma API de que você dependa aceite apenas Memory<T> argumentos. Isso é bom, mas esteja ciente das compensações envolvidas ao usar Memory<T> de forma síncrona.
Regra n° 2: usar ReadOnlySpan<T> ou ReadOnlyMemory<T> se o buffer for somente leitura
Nos exemplos anteriores, o DisplayBufferToConsole método lê apenas do buffer; ele não modifica o conteúdo do buffer. A assinatura do método deve ser alterada para a seguinte.
void DisplayBufferToConsole(ReadOnlyMemory<char> buffer);
Na verdade, se você combinar essa regra e a Regra nº 1, poderemos fazer ainda melhor e reescrever a assinatura do método da seguinte maneira:
void DisplayBufferToConsole(ReadOnlySpan<char> buffer);
O DisplayBufferToConsole método agora funciona com praticamente todos os tipos de buffer imagináveis: T[]armazenamento alocado com stackalloc e assim por diante. Você pode até mesmo passar um String diretamente nele! Para obter mais informações, consulte o problema do GitHub dotnet/docs #25551.
Regra nº 3: se o método aceitar Memória<T> e retornarvoid, você não deverá usar a instância de Memória<T> após o retorno do método>
Isso está relacionado ao conceito de "concessão" mencionado anteriormente. O período de uso de um método que retorna vazio na instância Memory<T> começa quando o método é iniciado e termina quando o método é finalizado. Considere o exemplo a seguir, que chama Log em um loop com base na entrada do 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 for um método totalmente síncrono, esse código se comportará conforme o esperado porque há apenas um consumidor ativo da instância de memória em um determinado momento.
Mas imagine, em vez disso, que Log tenha essa implementação.
// !!! 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);
});
}
Nessa implementação, Log viola sua concessão porque ainda tenta usar a Memory<T> instância em segundo plano depois que o método original é retornado. O Main método pode alterar o buffer enquanto Log tenta lê-lo, o que pode resultar em corrupção de dados.
Há várias maneiras de resolver isso:
O
Logmétodo pode retornar um Task em vez devoid, como a implementação a seguir doLogmétodo faz.// 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(); }); }Logem vez disso, pode ser implementado da seguinte maneira:// 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(); }); }
Regra n° 4: se o método aceitar o Memory<T> e retornar uma classe Task, não usar a instância de Memory<T> quando a classe Task passar para um estado terminal
Essa é apenas a variante assíncrona da Regra nº 3. O Log método do exemplo anterior pode ser escrito da seguinte maneira para cumprir esta regra:
// 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();
});
}
Aqui, "estado terminal" significa que a tarefa faz a transição para um estado concluído, com falha ou cancelado. Em outras palavras, "estado terminal" significa "qualquer coisa que cause atraso de lançamento ou continuação de execução".
Essa orientação se aplica a métodos que retornamTask, Task<TResult>ValueTask<TResult>ou qualquer tipo semelhante.
Regra nº 5: se o construtor aceitar Memória<T> como um parâmetro, os métodos de instância no objeto construído serão considerados consumidores da instância de Memória<T>
Considere o seguinte exemplo:
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);
}
}
Aqui, o construtor OddValueExtractor aceita um ReadOnlyMemory<int> como parâmetro de construtor, portanto, o próprio construtor é um consumidor da instância ReadOnlyMemory<int>, e todos os métodos da instância no valor retornado também são consumidores da instância original ReadOnlyMemory<int>. Isso significa que TryReadNextOddValue consome a ReadOnlyMemory<int> instância, mesmo que a instância não seja passada diretamente para o TryReadNextOddValue método.
Regra n° 6: quando há uma propriedade tipada configurável de Memory<T> (ou um método de instância equivalente) no tipo, presume-se que os métodos da instância nesse objeto sejam consumidores da instância de Memory<T>
Esta é realmente apenas uma variante da Regra nº 5. Essa regra existe porque se presume que definidores de propriedade ou métodos equivalentes capturam e persistem suas entradas; assim, os métodos de instância no mesmo objeto podem aproveitar o estado capturado.
O exemplo a seguir dispara esta regra:
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;
}
Regra n° 7: quando há uma referência de IMemoryOwner<T> é necessário descartá-la ou transferir a propriedade (mas não as duas coisas), em algum momento
Como uma Memory<T> instância pode ser apoiada por memória gerenciada ou não gerenciada, o proprietário deve chamar Dispose em IMemoryOwner<T> quando o trabalho realizado na instância Memory<T> estiver concluído. Como alternativa, o proprietário pode transferir a posse da instância para um componente diferente, e nesse momento o componente que a recebe se torna responsável por chamar IMemoryOwner<T> no momento apropriado (mais sobre isso posteriormente).
A falha ao chamar o Dispose método em uma IMemoryOwner<T> instância pode levar a vazamentos de memória não gerenciados ou a outra degradação de desempenho.
Essa regra também se aplica ao código que chama métodos de fábrica como MemoryPool<T>.Rent. O chamador torna-se o proprietário do objeto retornado IMemoryOwner<T> e é responsável por descartar a instância quando não for mais necessária.
Regra nº 8: se você tiver um parâmetro IMemoryOwner<T> na superfície da API, você estará aceitando a propriedade dessa instância
Aceitar uma instância desse tipo sinaliza que seu componente pretende assumir a propriedade dessa instância. Seu componente se torna responsável pelo descarte adequado de acordo com a Regra nº 7.
Qualquer componente que transfira a IMemoryOwner<T> propriedade da instância para um componente diferente não deve mais usar essa instância após a conclusão da chamada de método.
Importante
Se o seu construtor aceitar IMemoryOwner<T> como um parâmetro, o tipo deve implementar IDisposable, e o seu método Dispose deve chamar Dispose no objeto IMemoryOwner<T>.
Regra nº 9: se você estiver encapsulando um método p/invoke síncrono, sua API deverá aceitar Span<T> como um parâmetro
De acordo com a Regra nº 1, Span<T> geralmente é o tipo correto a ser usado para APIs síncronas. Você pode fixar instâncias Span<T> usando a palavra-chave fixed, como no exemplo a seguir.
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;
}
}
No exemplo anterior, pbData poderá ser nulo se, por exemplo, o intervalo de entrada estiver vazio. Se o método exportado exigir absolutamente que pbData seja não nulo, mesmo que cbData seja 0, o método poderá ser implementado da seguinte maneira:
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;
}
}
Regra nº 10: se você estiver encapsulando um método de p/invoke assíncrono, sua API deverá aceitar Memory<T> como um parâmetro
Como você não pode usar a fixed keyword em operações assíncronas, use o método Memory<T>.Pin para fixar instâncias Memory<T>, independentemente do tipo de memória contígua que a instância representa. O exemplo a seguir mostra como usar essa API para executar uma chamada p/invoke assíncrona.
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;
}