Nota
O acesso a esta página requer autorização. Podes tentar iniciar sessão ou mudar de diretório.
O acesso a esta página requer autorização. Podes tentar mudar de diretório.
O .NET inclui vários tipos que representam uma região contígua arbitrária da memória. Span<T> e ReadOnlySpan<T> são buffers de memória leves que envolvem referências a memória gerenciada ou não gerenciada. Como esses tipos só podem ser armazenados na pilha, eles não são adequados para cenários tais como chamadas de métodos assíncronas. Para resolver esse problema, o .NET 2.1 adicionou alguns tipos adicionais, incluindo Memory<T>, ReadOnlyMemory<T>, IMemoryOwner<T>e 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 gerido.
Ambos Span<T> e Memory<T> são "wrappers" ou envoltórios de 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 de forma eficiente para componentes no pipeline, que podem processá-los e, opcionalmente, modificar o buffer. Como Memory<T> 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 gestão do tempo de vida
Os buffers podem ser passados entre APIs e, às vezes, podem ser acessados de vários threads, portanto, esteja ciente de como o tempo de vida de um buffer é gerenciado. Existem três conceitos centrais:
Propriedade. O proprietário de uma instância de buffer é responsável pelo gerenciamento do 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 componente que criou o buffer ou que o recebeu de uma fábrica é o proprietário. A propriedade também pode ser transferida; O Componente-A pode ceder o controle do buffer para o Componente-B, momento em que o Componente-A não pode mais usar o buffer, e o Componente-B torna-se responsável por destruir o buffer quando ele não estiver mais em uso.
Consumo. O consumidor de uma instância de buffer tem permissão para usar a instância de buffer lendo a partir dela e, possivelmente, gravando nela. Os buffers podem ter um consumidor de cada 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.
Arrendamento. O arrendamento é o período de tempo durante o qual um determinado componente pode ser o consumidor do buffer.
O exemplo de pseudocódigo a seguir ilustra esses três conceitos.
Buffer no pseudo-código representa um Memory<T> ou Span<T> buffer do tipo Char. O Main método instancia o buffer, chama o WriteInt32ToBuffer método para gravar a representação de cadeia de caracteres de um inteiro para o buffer e, em seguida, chama o DisplayBufferToConsole método 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, por isso, é também o seu 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> sequer 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. Note também que "consumidor", neste contexto, não implica uma visão somente de leitura do buffer; os consumidores podem modificar o conteúdo do buffer, como WriteInt32ToBuffer faz, caso recebam uma visão de leitura/gravação do buffer.
O WriteInt32ToBuffer método tem uma concessão em (pode consumir) o buffer entre o início da chamada de método e o tempo que o método retorna. Da mesma forma, 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á API para gerenciamento de locação; uma "locação" é uma questão conceitual.)
Memória<T> e o modelo de 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 suporta dois modelos de propriedade:
- Um modelo que suporta a propriedade única. Um buffer tem um único proprietário durante toda a sua vida útil.
- Um modelo que suporta a transferência de propriedade. A propriedade de um buffer pode ser transferida de seu proprietário original (seu criador) para outro componente, que então se torna responsável pelo gerenciamento do 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> suporta ambos os modelos de propriedade. O componente que tem uma IMemoryOwner<T> referência possui o 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 using instrução:
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 eDisplayBufferToConsoleaceitam 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 finalidade. Para refletir este facto, poderia ter aceitado um argumento do tipo ReadOnlyMemory<T>. Para obter mais informações sobre ReadOnlyMemory<T>, consulte a Regra #2: Utilize ReadOnlySpan<T> ou ReadOnlyMemory<T> se o buffer for apenas para leitura.
Instâncias T de memória<"> sem dono"
Você pode criar uma Memory<T> instância sem usar IMemoryOwner<T>o . Nesse caso, a propriedade do buffer é implícita em vez de explícita, e apenas o modelo de proprietário único é suportado. Pode fazê-lo da seguinte forma:
- Chamando diretamente um dos Memory<T> construtores, passando um
T[], como mostra o 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á instância IMemoryOwner<T> que facilite a transferência. (Como alternativa, você também pode imaginar que o coletor de lixo do tempo de execução possui o buffer e todos os métodos apenas consomem o buffer.)
Diretrizes de uso
Como um bloco de memória é possuído, 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 o uso de Memory<T> e Span<T>. As diretrizes são necessárias porque é possível que um componente:
- Mantenha uma referência a um bloco de memória depois que seu proprietário o liberou.
- Opere num buffer ao mesmo tempo em que outro componente está a operá-lo, resultando em dados corruptos no buffer.
Embora a alocação na pilha de Span<T> otimize o desempenho e torne Span<T> o tipo preferido para operar sobre um bloco de memória, ela também submete Span<T> a restrições significativas. É importante saber quando usar um Span<T> e quando usar Memory<T>.
A seguir estão nossas recomendações para o uso Memory<T> bem-sucedido e seus tipos relacionados. Orientações que se aplicam a Memory<T> e Span<T> também se aplicam a ReadOnlyMemory<T> e ReadOnlySpan<T>, salvo indicação em contrário.
-
Regra #1: Para uma API síncrona, use
Span<T>em vez deMemory<T>como parâmetro, se possível -
Regra #2: Use
ReadOnlySpan<T>ouReadOnlyMemory<T>se o buffer deve ser somente leitura -
Regra #3: Se o seu método aceita
Memory<T>e retornavoid, não deves usar a instância comMemory<T>depois que o método retornar -
Regra #4: Se o método aceitar e retornar uma
Memory<T>Tarefa, você não deverá usar aMemory<T>instância após a transição da Tarefa para um estado terminal -
Regra #5: Se o construtor aceitar
Memory<T>como parâmetro, os métodos de instância no objeto construído serão assumidos como consumidores daMemory<T>instância - Regra #6: Se tiver uma propriedade ajustável com tipo
Memory<T>(ou um método de instância equivalente) do seu tipo, os métodos de instância nesse objeto são considerados consumidores da instânciaMemory<T> -
Regra #7: Se você tiver uma
IMemoryOwner<T>referência, você deve em algum momento descartá-la ou transferir sua propriedade (mas não ambas) -
Regra #8: Se você tiver um
IMemoryOwner<T>parâmetro em sua superfície de API, estará aceitando a propriedade dessa instância -
Regra #9: Se você estiver encapsulando um método P/Invoke síncrono, sua API deverá aceitar
Span<T>como parâmetro -
Regra #10: Se você estiver encapsulando um método assíncrono p/invoke, sua API deverá aceitar
Memory<T>como parâmetro
Regra #1: Para uma API síncrona, use Span<T> em vez de Memory<T> como parâmetro, se possível
Span<T> é mais versátil do que Memory<T> e pode representar uma maior variedade de buffers de memória contíguos. Span<T> também oferece melhor desempenho do que Memory<T>. Finalmente, pode usar a propriedade Memory<T>.Span para converter uma instância de Memory<T> em Span<T>, embora a conversão de Span<T> para Memory<T> não seja possível. Portanto, se seus chamadores tiverem uma Memory<T> instância, eles poderão chamar seus métodos com Span<T> parâmetros de qualquer maneira.
Usar um parâmetro de tipo Span<T> em vez de tipo Memory<T> também ajuda a escrever uma implementação de método de consumo correta. Você receberá automaticamente verificações em tempo de compilação para garantir que não está tentando acessar o buffer além do espaço alocado pelo método (mais sobre isso mais tarde).
Às vezes, você terá que usar um Memory<T> parâmetro em vez de um Span<T> parâmetro, mesmo que seja totalmente síncrono. Talvez uma API da qual você depende aceite apenas Memory<T> argumentos. Isso é bom, mas esteja ciente das compensações envolvidas ao usar Memory<T> de forma síncrona.
Regra #2: Use ReadOnlySpan<T> ou ReadOnlyMemory<T> se o buffer deve ser somente leitura
Nos exemplos anteriores, o DisplayBufferToConsole método só lê a partir do buffer, não modifica o conteúdo do buffer. A assinatura do método deve ser alterada para o seguinte.
void DisplayBufferToConsole(ReadOnlyMemory<char> buffer);
Na verdade, se você combinar essa regra e a Regra #1, podemos 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 stackaloc e assim por diante. Você pode até passar um String diretamente para ele! Para obter mais informações, consulte GitHub issue dotnet/docs #25551.
Regra #3: Se o seu método aceita Memory<T> e retorna void, você não deve usar a instância Memory<T> depois de o seu método ter retornado
Isto está relacionado com o conceito de "arrendamento" mencionado anteriormente. A concessão de um método de retorno de vazio na Memory<T> instância começa quando o método é inserido e termina quando o método é encerrado. 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 a qualquer momento.
Mas imagine, em vez disso, que Log tem 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 retornou. O Main método pode mutar o buffer enquanto Log tenta ler a partir dele, o que pode resultar em corrupção de dados.
Há várias maneiras de resolver isso:
O método
Logpode retornar um Task em vez de umvoid, como faz a seguinte implementação do métodoLog.// 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(); }); }Logpode, em vez disso, ser implementado da seguinte forma:// 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 #4: Se o seu método aceitar uma Memória<T> e retornar uma Tarefa, não deve usar a instância Memória<T> após a transição da Tarefa para um estado terminal.
Esta é apenas a variante assíncrona da Regra #3. O Log método do exemplo anterior pode ser escrito da seguinte forma 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 transita para um estado concluído, com falha ou cancelado. Em outras palavras, "estado terminal" significa "qualquer coisa que faça aguardar para lançar ou continuar a execução".
Esta orientação aplica-se a métodos que retornam Task, Task<TResult>, ValueTask<TResult>, ou qualquer tipo semelhante.
Regra #5: Se o construtor aceitar Memory<T> como um parâmetro, os métodos de instância no objeto construído serão assumidos como consumidores da instância Memory<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 OddValueExtractor construtor 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 de instância de ReadOnlyMemory<int> no valor retornado também são consumidores da instância original . Isso significa que consome a TryReadNextOddValue instância, mesmo que ReadOnlyMemory<int> ela não seja passada diretamente para o TryReadNextOddValue método.
Regra #6: Se tiver uma propriedade passível de definição do tipo Memory<T> (ou um método de instância equivalente) no seu tipo, os métodos de instância nesse objeto serão considerados consumidores da instância Memory<T>
Esta é realmente apenas uma variante da Regra #5. Essa regra existe porque se presume que os setters de propriedades ou métodos equivalentes capturam e persistem suas entradas, portanto, os métodos de instância no mesmo objeto podem utilizar o estado capturado.
O exemplo a seguir aciona essa 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 #7: Se tens uma referência IMemoryOwner<T>, deves, em algum momento, eliminá-la ou transferir a sua propriedade (mas não ambas)
Como uma instância Memory<T> pode ser suportada por memória gerenciada ou não gerenciada, o proprietário deve chamar Dispose em IMemoryOwner<T> quando o trabalho na instância Memory<T> for concluído. Alternativamente, o proprietário pode transferir a propriedade da instância IMemoryOwner<T> para um componente diferente, momento em que o componente adquirente torna-se então responsável por chamar Dispose no momento apropriado (mais detalhes sobre isso mais tarde).
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 outra degradação de desempenho.
Esta 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 valor retornado IMemoryOwner<T> e é responsável por eliminar a instância quando terminar.
Regra #8: Se você tiver um parâmetro IMemoryOwner<T> em sua superfície de API, estará aceitando a propriedade dessa instância
Aceitar uma instância desse tipo sinaliza que seu componente pretende se apropriar dessa instância. O seu componente torna-se responsável pela eliminação adequada de acordo com a Regra #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 aceita IMemoryOwner<T> como parâmetro, o seu tipo deve implementar IDisposable, e o método Dispose deve chamar Dispose no objeto IMemoryOwner<T>.
Regra #9: Se você estiver encapsulando um método p/invoke síncrono, sua API deverá aceitar Span<T> como parâmetro
De acordo com a Regra #1, Span<T> geralmente é o tipo correto a ser usado para APIs síncronas. Você pode fixar Span<T> ocorrências por meio da 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 pode ser nulo se, por exemplo, a extensão de entrada estiver vazia. Se o método exportado exigir de forma absoluta que pbData seja diferente de nulo, mesmo que cbData seja 0, o método pode 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 #10: Se você estiver encapsulando um método assíncrono p/invoke, sua API deverá aceitar a memória<T> como parâmetro
Como não é possível usar a fixed palavra-chave em operações assíncronas, você usa o Memory<T>.Pin método para fixar Memory<T> instâncias, 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 assíncrona 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;
}