Prefácio da tradução
Ao contrário dos artigos científicos, é difícil traduzir artigos desse tipo "perto do texto", e uma adaptação bastante forte deve ser feita. Por esse motivo, peço desculpas por algumas liberdades, da minha parte, ao lidar com o texto do artigo original. Sou guiado por apenas um objetivo - tornar a tradução compreensível, mesmo que em alguns lugares ela se desvie fortemente do artigo original. Eu ficaria grato por críticas construtivas e correções / acréscimos à tradução.
1. Introdução
O namespace System.Threading.Tasks
e a classe Task
foram introduzidos pela primeira vez no .NET Framework 4. Desde então, esse tipo e sua classe derivada Task<TResult>
entraram firmemente na prática de programação no .NET e tornaram-se aspectos-chave do modelo assíncrono. implementado em C # 5, com seu async/await
. Neste artigo, falarei sobre novos tipos de ValueTask/ValueTask<TResult>
que foram introduzidos com o objetivo de aumentar o desempenho do código assíncrono, nos casos em que a sobrecarga ao trabalhar com memória desempenha um papel fundamental.

Tarefa
Task
serve a vários propósitos, mas o principal é a "promessa" - um objeto que representa a capacidade de aguardar a conclusão de uma operação. Você inicia a operação e obtém a Task
. Esta Task
será concluída quando a operação em si for concluída. Nesse caso, existem três opções:
- A operação é concluída de forma síncrona no encadeamento do iniciador. Por exemplo, ao acessar alguns dados que já estão no buffer .
- A operação é executada de forma assíncrona, mas consegue concluir quando o iniciador recebe a
Task
. Por exemplo, ao executar acesso rápido a dados que ainda não foram armazenados em buffer - A operação é executada de forma assíncrona e termina após o iniciador receber a
Task
exemplo é o recebimento de dados em uma rede .
Para obter o resultado de uma chamada assíncrona, o cliente pode bloquear o segmento de chamada enquanto aguarda a conclusão, o que geralmente contradiz a idéia de assincronia, ou fornecer um método de retorno de chamada que será executado após a conclusão da operação assíncrona. O modelo de retorno de chamada no .NET 4 foi apresentado explicitamente, usando o método ContinueWith
de um objeto da classe Task
, que recebeu um delegado chamado após a conclusão da operação assíncrona.
SomeOperationAsync().ContinueWith(task => { try { TResult result = task.Result; UseResult(result); } catch (Exception e) { HandleException(e); } });
Com o .NET Frmaework 4.5 e C # 5, o resultado de uma operação assíncrona foi simplificado com a introdução das palavras-chave async/await
e o mecanismo por trás delas. Esse mecanismo, o código gerado, é capaz de otimizar todos os casos mencionados acima, manipulando corretamente a conclusão, apesar do caminho em que foi atingido.
TResult result = await SomeOperationAsync(); UseResult(result);
A classe Task
é bastante flexível e possui várias vantagens. Por exemplo, você pode "esperar" um objeto dessa classe várias vezes, o resultado de forma competitiva, por qualquer número de consumidores. Instâncias de uma classe podem ser armazenadas em um dicionário para qualquer número de chamadas subsequentes, com o objetivo de "aguardar" no futuro. Os cenários descritos permitem considerar objetos de Task
como um tipo de cache de resultados obtidos de forma assíncrona. Além disso, o Task
fornece a capacidade de bloquear o thread em espera até que a operação seja concluída, se o script exigir. Há também o chamado. combinadores para várias estratégias para aguardar a conclusão de conjuntos de tarefas, por exemplo, "Task.WhenAny" - aguardando assincronamente a conclusão da primeira de muitas tarefas.
Porém, o caso de uso mais comum é simplesmente iniciar uma operação assíncrona e aguardar o resultado de sua execução. Um caso tão simples, bastante comum, não requer a flexibilidade acima:
TResult result = await SomeOperationAsync(); UseResult(result);
Isso é muito semelhante à forma como escrevemos código síncrono (por exemplo, TResult result = SomeOperation();
). Esta opção é naturalmente traduzida em async/await
.
Além disso, por todos os seus méritos, o tipo de Task
tem uma falha em potencial. Task
é uma classe, o que significa que toda operação que cria uma instância de uma tarefa aloca um objeto no heap. Quanto mais objetos criamos, mais trabalho é exigido do GC e mais recursos são gastos no trabalho do coletor de lixo, recursos que podem ser usados para outros fins. Isso se torna um problema claro para o código, no qual, por um lado, as instâncias de Task
são criadas com freqüência e, por outro lado, o que aumenta os requisitos de taxa de transferência e desempenho.
O tempo de execução e as principais bibliotecas, em muitas situações, conseguem mitigar esse efeito. Por exemplo, se você escrever um método como o abaixo:
public async Task WriteAsync(byte value) { if (_bufferedCount == _buffer.Length) { await FlushAsync(); } _buffer[_bufferedCount++] = value; }
e, na maioria das vezes, haverá espaço suficiente no buffer, a operação será finalizada de forma síncrona. Nesse caso, não há nada de especial na tarefa retornada, não há valor de retorno e a operação já está concluída. Em outras palavras, estamos lidando com a Task
, o equivalente a uma operação void
síncrona. Nessas situações, o tempo de execução simplesmente armazena em cache o objeto Task
e o usa sempre como resultado para qualquer async Task
- um método que termina de forma síncrona ( Task.ComletedTask
). Outro exemplo, digamos que você escreva:
public async Task<bool> MoveNextAsync() { if (_bufferedCount == 0) { await FillBuffer(); } return _bufferedCount > 0; }
Suponha, da mesma maneira, que na maioria dos casos, haja alguns dados no buffer. O método verifica _bufferedCount
, vê que a variável é maior que zero e retorna true
. Somente se no momento da verificação os dados não foram armazenados em buffer, uma operação assíncrona é necessária. Seja como for, existem apenas dois resultados lógicos possíveis ( true
e false
) e apenas dois estados de retorno possíveis por meio da Task<bool>
. Com base na conclusão síncrona, ou assíncrona, mas antes de sair do método, o tempo de execução armazena em cache duas instâncias da Task<bool>
(uma para true
e outra para false
) e retorna a desejada, evitando alocações adicionais. A única opção quando você precisa criar um novo objeto Task<bool>
é um caso de execução assíncrona, que termina após o "retorno". Nesse caso, o método precisa criar um novo objeto Task<bool>
, porque no momento da saída do método, o resultado da conclusão da operação ainda não é conhecido. O objeto retornado deve ser exclusivo, porque ele armazenará o resultado da operação assíncrona.
Existem outros exemplos de armazenamento em cache semelhante no tempo de execução. Mas essa estratégia não é aplicável em todos os lugares. Por exemplo, o método:
public async Task<int> ReadNextByteAsync() { if (_bufferedCount == 0) { await FillBuffer(); } if (_bufferedCount == 0) { return -1; } _bufferedCount--; return _buffer[_position++]; }
também frequentemente termina de forma síncrona. Mas, diferentemente do exemplo anterior, esse método retorna um resultado inteiro que possui aproximadamente quatro bilhões de valores possíveis. Para armazenar em cache a Task<int>
, nessa situação, seriam necessárias centenas de gigabytes de memória. O ambiente aqui também suporta um pequeno cache para a Task<int>
, para vários valores pequenos. Portanto, por exemplo, se a operação for concluída de forma síncrona (os dados estão presentes no buffer), com um resultado de 4, o cache será usado. Porém, se o resultado, embora síncrono, for 42, um novo objeto Task<int>
será criado, semelhante à chamada Task.FromResult(42)
.
Muitas implementações de bibliotecas tentam atenuar essas situações, suportando seus próprios caches. Um exemplo é a sobrecarga do MemoryStream.ReadAsync
. Esta operação, introduzida no .NET Framework 4.5, sempre termina de forma síncrona, porque é apenas uma leitura da memória. ReadAsync
retorna uma Task<int>
onde o resultado inteiro representa o número de bytes lidos. Muitas vezes, no código, ocorre uma situação quando o ReadAsync
usado em um loop. Além disso, se houver os seguintes sintomas:
- O número de bytes solicitados não muda para a maioria das iterações do loop;
- Na maioria das iterações, o
ReadAsync
pode ler o número solicitado de bytes.
Ou seja, para chamadas repetidas, o ReadAsync
é executado de forma síncrona e retorna um objeto Task<int>
, com o mesmo resultado de iteração para iteração. É lógico que o MemoryStream
cache a última tarefa concluída com êxito e, para todas as chamadas subseqüentes, se o novo resultado corresponder ao anterior, ele retornará uma instância do cache. Se o resultado não corresponder, o Task.FromResult
usado para criar uma nova instância, que, por sua vez, também será armazenada em cache antes de retornar.
Porém, há muitos casos em que uma operação é forçada a criar novos objetos Task<TResult>
, mesmo quando concluídos de forma síncrona.
ValueTask <TResult> e conclusão síncrona
Tudo isso serviu de motivação para a introdução de um novo tipo de ValueTask<TResult>
no .NET Core 2.0. Além disso, por meio do pacote nuget System.Threading.Tasks.Extensions
, esse tipo foi disponibilizado em outras versões do .NET.
ValueTask<TResult>
foi introduzido no .NET Core 2.0 como uma estrutura capaz de agrupar TResult
ou Task<TResult>
. Isso significa que objetos desse tipo podem ser retornados do método async
. A primeira vantagem da introdução desse tipo é imediatamente visível: se o método for concluído com êxito e de forma síncrona, não será necessário criar nada no heap, apenas o suficiente para criar uma instância do ValueTask<TResult>
com o valor resultante. Somente se o método sair assincronamente, precisamos criar uma Task<TResult>
. Nesse caso, ValueTask<TResult>
usado como um invólucro na Task<TResult>
. A decisão de tornar o ValueTask<TResult>
capaz de agregar a Task<TResult>
foi tomada com o objetivo de otimização: em caso de sucesso e em caso de falha, o método assíncrono cria a Task<TResult>
, do ponto de vista da otimização da memória, é melhor agregar a Task<TResult>
próprio objeto Task<TResult>
que para manter campos adicionais no ValueTask<TResult>
para vários casos de conclusão (por exemplo, para armazenar uma exceção).
Dado o exposto, não é mais necessário armazenar em cache métodos como o MemoryStream.ReadAsync
acima, mas pode ser implementado da seguinte maneira:
public override ValueTask<int> ReadAsync(byte[] buffer, int offset, int count) { try { int bytesRead = Read(buffer, offset, count); return new ValueTask<int>(bytesRead); } catch (Exception e) { return new ValueTask<int>(Task.FromException<int>(e)); } }
ValueTask <TResult> e finalização assíncrona
Ter a capacidade de escrever métodos assíncronos que não exigem alocações de memória adicionais para o resultado, com conclusão síncrona, é realmente uma grande vantagem. Conforme mencionado acima, esse era o principal objetivo da introdução do novo tipo ValueTask<TResult>
no .NET Core 2.0. Todos os novos métodos que devem ser usados nas "estradas quentes" agora usam ValueTask<TResult>
vez de Task<TResult>
como o tipo de retorno. Por exemplo, uma nova sobrecarga do método ReadAsync
para Stream
, no .NET Core 2.1 (que usa a Memory<byte>
vez de byte[]
como parâmetro), retorna uma instância de ValueTask<int>
. Isso permitiu reduzir significativamente o número de alocações ao trabalhar com fluxos (muitas vezes o método ReadAsync
termina de forma síncrona, como no exemplo com MemoryStream
).
No entanto, ao desenvolver serviços com alta largura de banda, em que a terminação assíncrona não é incomum, precisamos fazer o possível para evitar alocações adicionais.
Como mencionado anteriormente, no modelo async/await
, qualquer operação que seja concluída de forma assíncrona deve retornar um objeto exclusivo para aguardar a conclusão. Único porque servirá como um canal para realizar retornos de chamada. Observe, no entanto, que essa construção não diz nada sobre se o objeto de espera retornado pode ser reutilizado após a conclusão da operação assíncrona. Se um objeto puder ser reutilizado, a API poderá manter um pool para esses tipos de objetos. Mas, nesse caso, esse pool não pode suportar acesso simultâneo - um objeto do pool passará do estado "concluído" para o estado "não concluído" e vice-versa.
Para oferecer suporte à possibilidade de trabalhar com esses pools, a interface IValueTaskSource<TResult>
foi adicionada ao .NET Core 2.1 e a estrutura ValueTask<TResult>
foi expandida: agora os objetos desse tipo podem envolver não apenas os objetos do tipo TResult
ou Task<TResult>
, mas também instâncias de IValueTaskSource<TResult>
. A nova interface fornece funcionalidade básica que permite que objetos ValueTask<TResult>
trabalhem com IValueTaskSource<TResult>
da mesma maneira que com a Task<TResult>
:
public interface IValueTaskSource<out TResult> { ValueTaskSourceStatus GetStatus(short token); void OnCompleted( Action<object> continuation, object state, short token, ValueTaskSourceOnCompletedFlags flags); TResult GetResult(short token); }
GetStatus
destinado ao uso na propriedade ValueTask<TResult>.IsCompleted/IsCompletedSuccessfully
- permite descobrir se a operação foi concluída ou não (com êxito ou não). OnCompleted
usado no ValueTask<TResult>
para acionar um retorno de chamada. GetResult
usado para obter o resultado ou lançar uma exceção.
É improvável que a maioria dos desenvolvedores precise lidar com a interface IValueTaskSource<TResult>
, porque métodos assíncronos, quando retornados, ocultam-no atrás da ValueTask<TResult>
. A interface em si é direcionada principalmente para aqueles que desenvolvem APIs de alto desempenho e procura evitar trabalho desnecessário com muitos.
No .NET Core 2.1, existem vários exemplos desse tipo de API. O mais famoso deles são as novas sobrecargas dos métodos Socket.ReceiveAsync
e Socket.SendAsync
. Por exemplo:
public ValueTask<int> ReceiveAsync( Memory<byte> buffer, SocketFlags socketFlags, CancellationToken cancellationToken = default);
Objetos do tipo ValueTask<int>
são usados como o valor de retorno.
Se o método sair de forma síncrona, ele retornará uma ValueTask<int>
com o valor correspondente:
int result = …; return new ValueTask<int>(result);
Se a operação for concluída de forma assíncrona, será usado um objeto em cache que implementa a interface IValueTaskSource<TResult>
:
IValueTaskSource<int> vts = …; return new ValueTask<int>(vts);
A implementação do Socket
suporta um objeto em cache para recebimento e outro para envio de dados, desde que cada um deles seja usado sem concorrência (não, por exemplo, envio de dados competitivos). Essa estratégia reduz a quantidade de memória adicional alocada, mesmo no caso de execução assíncrona.
A otimização descrita do Socket
no .NET Core 2.1 teve um impacto positivo no desempenho do NetworkStream
. Sua sobrecarga é o método ReadAsync
da classe Stream
:
public virtual ValueTask<int> ReadAsync( Memory<byte> buffer, CancellationToken cancellationToken);
apenas delega o trabalho para o método Socket.ReceiveAsync
. Aumentar a eficiência do método soquete, em termos de trabalho com memória, aumenta a eficiência do método NetworkStream
.
ValueTask não genérico
Anteriormente, observei várias vezes que o objetivo original do ValueTask<T>
, no .NET Core 2.0, era otimizar casos de conclusão síncrona de métodos com um resultado "não vazio". Isso significa que não havia necessidade de uma ValueTask
não digitada: nos casos de conclusão síncrona, os métodos usam um singleton por meio da propriedade Task.CompletedTask
, e o tempo de execução dos métodos de async Task
também é recebido implicitamente.
Mas, com o advento da capacidade de evitar alocações desnecessárias e com execução assíncrona, a necessidade de um ValueTask
não digitado novamente se tornou relevante. Por esse motivo, no .NET Core 2.1, introduzimos ValueTask
não ValueTask
e IValueTaskSource
. Eles são análogos dos tipos genéricos correspondentes e são usados da mesma maneira, mas para métodos com um retorno vazio ( void
).
Implementar IValueTaskSource / IValueTaskSource <T>
A maioria dos desenvolvedores não precisará implementar essas interfaces. E sua implementação não é uma tarefa fácil. Se você decidir implementá-los, então, dentro do .NET Core 2.1, há várias implementações que podem servir como exemplos:
Para simplificar essas tarefas (implementações de IValueTaskSource / IValueTaskSource<T>
), planejamos introduzir o tipo ManualResetValueTaskSourceCore<TResult>
no .NET Core 3.0. Essa estrutura encapsulará toda a lógica necessária. A ManualResetValueTaskSourceCore<TResult>
pode ser usada em outro objeto que implementa IValueTaskSource<TResult>
e / ou IValueTaskSource
e delegar a maior parte do trabalho. Você pode aprender mais sobre isso em ttps: //github.com/dotnet/corefx/issues/32664.
O modelo correto para usar ValueTasks
Mesmo um exame superficial ValueTask
que ValueTask
e ValueTask<TResult>
mais limitados que Task
e Task<TResult>
. E isso é normal, até desejável, porque o objetivo principal é aguardar a conclusão da execução assíncrona.
Em particular, surgem limitações significativas devido ao fato de que ValueTask
e ValueTask<TResult>
podem agregar objetos reutilizáveis. Em geral, as seguintes operações * NUNCA devem ser executadas ao usar ValueTask
/ ValueTask<TResult>
* ( deixe-me reformular através de "Never" *):
- Nunca use o mesmo
ValueTask
/ ValueTask<TResult>
repetidamente
Motivação: As instâncias Task
e Task<TResult>
nunca passam do estado "concluído" para o estado "incompleto", podemos usá-las para aguardar o resultado quantas vezes quisermos - após a conclusão, sempre obteremos o mesmo resultado. Pelo contrário, desde ValueTask
/ ValueTask<TResult>
, eles podem atuar como invólucros sobre objetos reutilizados, o que significa que seu estado pode mudar, porque o estado dos objetos reutilizados muda por definição - para passar de "concluído" para "incompleto" e vice-versa.
- Nunca
ValueTask
/ ValueTask<TResult>
no modo competitivo.
Motivação: um objeto empacotado espera trabalhar com apenas um retorno de chamada, de um único consumidor de cada vez, e tentar competir antecipadamente pode facilmente levar a condições de corrida e sutis erros de programação. Expectativas competitivas, esta é uma das opções descritas acima, múltiplas expectativas . Observe que a Task
/ Task<TResult>
permite qualquer número de expectativas competitivas.
- Nunca use
.GetAwaiter().GetResult()
até que a operação seja concluída .
Motivação: As implementações de IValueTaskSource
/ IValueTaskSource<TResult>
não devem suportar o bloqueio até que a operação seja concluída. De fato, o bloqueio leva a uma condição de corrida; é improvável que esse seja o comportamento esperado por parte do consumidor. Enquanto Task
/ Task<TResult>
permite fazer isso, bloqueando o encadeamento de chamada até que a operação seja concluída.
Mas e se, no entanto, você precisar ValueTask
uma das operações descritas acima e o método chamado retornar instâncias de ValueTask
/ ValueTask<TResult>
? Para esses casos, o ValueTask
/ ValueTask<TResult>
fornece o método .AsTask()
. Ao chamar esse método, você obterá uma instância de Task
/ Task<TResult>
e já poderá executar a operação necessária. Reutilizar o objeto original após chamar .AsTask()
não é permitido .
: ValueTask
/ ValueTask<TResult>
, ( await
) (, .ConfigureAwait(false)
), .AsTask()
, ValueTask
/ ValueTask<TResult>
.
// Given this ValueTask<int>-returning method… public ValueTask<int> SomeValueTaskReturningMethodAsync(); … // GOOD int result = await SomeValueTaskReturningMethodAsync(); // GOOD int result = await SomeValueTaskReturningMethodAsync().ConfigureAwait(false); // GOOD Task<int> t = SomeValueTaskReturningMethodAsync().AsTask(); // WARNING ValueTask<int> vt = SomeValueTaskReturningMethodAsync(); ... // storing the instance into a local makes it much more likely it'll be misused, // but it could still be ok // BAD: awaits multiple times ValueTask<int> vt = SomeValueTaskReturningMethodAsync(); int result = await vt; int result2 = await vt; // BAD: awaits concurrently (and, by definition then, multiple times) ValueTask<int> vt = SomeValueTaskReturningMethodAsync(); Task.Run(async () => await vt); Task.Run(async () => await vt); // BAD: uses GetAwaiter().GetResult() when it's not known to be done ValueTask<int> vt = SomeValueTaskReturningMethodAsync(); int result = vt.GetAwaiter().GetResult();
, "", , ( , ).
ValueTask
/ ValueTask<TResult>
, . , IsCompleted
true
, ( , ), — false
, IsCompletedSuccessfully
true
. " " , , , , , . await
/ .AsTask()
.Result
. , SocketsHttpHandler
.NET Core 2.1, .ReadAsync
, ValueTask<int>
. , , , . , .. . Porque , , , , :
int bytesRead; { ValueTask<int> readTask = _connection.ReadAsync(buffer); if (readTask.IsCompletedSuccessfully) { bytesRead = readTask.Result; } else { using (_connection.RegisterCancellation()) { bytesRead = await readTask; } } }
, .. ValueTask<int>
, .Result
, await
, .
API ValueTask / ValueTask<TResult>?
, . Task
/ ValueTask<TResult>
.
, Task
/ Task<TResult>
. , "" / , Task
/ Task<TResult>
. , , ValueTask<TResult>
Task<TResult>
: , , await
Task<TResult>
ValueTask<TResult>
. , (, API Task
Task<bool>
), , , Task
( Task<bool>
). , ValueTask
/ ValueTask<TResult>
. , async-, ValueTask
/ ValueTask<TResult>
, .
, ValueTask
/ ValueTask<TResult>
, :
- , API ,
- API ,
- , , , .
, abstract
/ virtual
, , / ?
O que vem a seguir?
.NET, API, Task
/ Task<TResult>
. , , API c ValueTask
/ ValueTask<TResult>
, . IAsyncEnumerator<T>
, .NET Core 3.0. IEnumerator<T>
MoveNext
, . — IAsyncEnumerator<T>
MoveNextAsync
. , Task<bool>
, , . , , , ( ), , , await foreach
-, , MoveNextAsync
, ValueTask<bool>
. , , , " " , . , C# , .
