Expressões de tarefas
Este artigo descreve o suporte em F# para expressões de tarefa, que são semelhantes a expressões assíncronas, mas permitem que você crie tarefas .NET diretamente. Assim como as expressões assíncronas, as expressões de tarefa executam o código de forma assíncrona, ou seja, sem bloquear a execução de outro trabalho.
Código assíncrono normalmente é criado usando expressões assíncronas. O uso de expressões de tarefa é preferencial ao interoperar extensivamente com as bibliotecas do .NET que criam ou consomem as tarefas do .NET. As expressões de tarefa também podem melhorar o desempenho e a experiência de depuração. No entanto, as expressões de tarefa vêm com algumas limitações, que são descritas posteriormente neste artigo.
Syntax
task { expression }
Na sintaxe anterior, a computação representada por expression
é configurada para ser executada como uma tarefa .NET. A tarefa é iniciada imediatamente após a execução desse código e é executada no thread atual até que sua primeira operação assíncrona seja executada (por exemplo, uma suspensão assíncrona, E/S assíncrona ou outra operação assíncrona primitiva). O tipo da expressão é Task<'T>
, onde 'T
é o tipo retornado pela expressão quando a palavra-chave return
é usada.
Associação usando let!
Em uma expressão de tarefa, algumas expressões e operações são síncronas e algumas são assíncronas. Quando você aguarda o resultado de uma operação assíncrona, em vez de uma associação let
comum, você usa let!
. O efeito de let!
é habilitar a execução para continuar em outras computações ou threads, à medida que a computação está sendo executada. Após o retorno do lado direito da associação let!
, o restante da tarefa retoma a execução.
O código a seguir mostra a diferença entre let
e let!
. A linha de código que usa let
apenas cria uma tarefa como um objeto que você pode aguardar mais tarde usando, por exemplo, task.Wait()
ou task.Result
. A linha de código que usa let!
inicia a tarefa e aguarda seu resultado.
// let just stores the result as a task.
let (result1 : Task<int>) = stream.ReadAsync(buffer, offset, count, cancellationToken)
// let! completes the asynchronous operation and returns the data.
let! (result2 : int) = stream.ReadAsync(buffer, offset, count, cancellationToken)
As expressões task { }
do F# podem aguardar os seguintes tipos de operações assíncronas:
- Tarefas do .NET, Task<TResult> e Task não genérica.
- Tarefas de valor do .NET, ValueTask<TResult> e ValueTask não genérica.
- Computações assíncronas
Async<T>
do F#. - Qualquer objeto seguindo o padrão "GetAwaiter" especificado em F# RFC FS-1097.
Expressões return
Nas expressões de tarefa, return expr
é usado para retornar o resultado de uma tarefa.
Expressões return!
Nas expressões de tarefa, return! expr
é usado para retornar o resultado de outra tarefa. É equivalente a usar let!
e retornar imediatamente o resultado.
Fluxo de controle
As expressões de tarefa podem incluir as construções de fluxo de controle for .. in .. do
, while .. do
, try .. with ..
, try .. finally ..
, if .. then .. else
e if .. then ..
. Estas, por sua vez, podem incluir outras construções de tarefas, exceto os manipuladores with
e finally
, que são executados de forma síncrona. Se você precisar de uma try .. finally ..
assíncrona, use uma associação use
em combinação com um objeto do tipo IAsyncDisposable
.
Associações use
e use!
Nas expressões de tarefa, as ligações use
podem ser vinculadas a valores do tipo IDisposable ou IAsyncDisposable. Nesse último caso, a operação de limpeza de descarte é executada de forma assíncrona.
Além do let!
, você pode usar use!
para executar associações assíncronas. A diferença entre let!
e use!
é a mesma diferença que há entre let
e use
. Para use!
, o objeto é descartado no fechamento do escopo atual. Observe que em F# 6, use!
não permite que um valor seja inicializado como nulo, embora use
permita.
Tarefas de valor
Tarefas de valor são estruturas usadas para evitar alocações na programação baseada em tarefas. Uma tarefa de valor é um valor efêmero que se transformou em uma tarefa real usando .AsTask()
.
Para criar uma tarefa de valor a partir de uma expressão de tarefa, use |> ValueTask<ReturnType>
ou |> ValueTask
. Por exemplo:
let makeTask() =
task { return 1 }
makeTask() |> ValueTask<int>
Adicionando tokens de cancelamento e verificações de cancelamento
Ao contrário das expressões assíncronas do F#, as expressões de tarefa não passam implicitamente um token de cancelamento e não realizam verificações de cancelamento implicitamente. Se seu código requer um token de cancelamento, você deve especificar o token de cancelamento como um parâmetro. Por exemplo:
open System.Threading
let someTaskCode (cancellationToken: CancellationToken) =
task {
cancellationToken.ThrowIfCancellationRequested()
printfn $"continuing..."
}
Se você pretende tornar seu código cancelável corretamente, verifique cuidadosamente se você passa o token de cancelamento para todas as operações da biblioteca .NET que dão suporte ao cancelamento. Por exemplo, Stream.ReadAsync
tem várias sobrecargas, uma das quais aceita um token de cancelamento. Se você não usar essa sobrecarga, essa operação de leitura assíncrona específica não será cancelável.
Tarefas em segundo plano
Por padrão, as tarefas .NET são agendadas usando SynchronizationContext.Current se presente. Isso permite que as tarefas funcionem como agentes cooperativos e intercalados em execução em um thread de interface do usuário sem bloquear a interface do usuário. Se não estiver presente, as continuações de tarefas serão agendadas para o pool de threads .NET.
Na prática, muitas vezes é desejável que o código da biblioteca que gera tarefas ignore o contexto de sincronização e, em vez disso, sempre alterne para o pool de threads .NET, se necessário. Você pode conseguir isso usando backgroundTask { }
:
backgroundTask { expression }
Uma tarefa em segundo plano ignora qualquer SynchronizationContext.Current
no seguinte sentido: se iniciada em um thread com SynchronizationContext.Current
não nulo, ela alterna para um thread em segundo plano no conjunto de threads usando Task.Run
. Se iniciado em um thread com nulo SynchronizationContext.Current
, ele é executado nesse mesmo thread.
Observação
Na prática, isso significa que as chamadas para ConfigureAwait(false)
normalmente não são necessárias no código de tarefa do F#. Em vez disso, as tarefas que devem ser executadas em segundo plano devem ser criadas usando backgroundTask { ... }
. Qualquer associação de tarefa externa a uma tarefa em segundo plano será ressincronizada com o SynchronizationContext.Current
na conclusão da tarefa em segundo plano.
Limitações de tarefas relacionadas a chamadas tail
Ao contrário das expressões assíncronas do F#, as expressões de tarefa não oferecem suporte a chamadas tail. Ou seja, quando return!
é executado, a tarefa atual é registrada como aguardando a tarefa cujo resultado está sendo retornado. Isso significa que funções e métodos recursivos implementados usando expressões de tarefa podem criar cadeias ilimitadas de tarefas, e estas podem usar pilha ou heap ilimitados. Por exemplo, considere o seguinte código:
let rec taskLoopBad (count: int) : Task<string> =
task {
if count = 0 then
return "done!"
else
printfn $"looping..., count = {count}"
return! taskLoopBad (count-1)
}
let t = taskLoopBad 10000000
t.Wait()
Esse estilo de codificação não deve ser usado com expressões de tarefa – ele criará uma cadeia de 1.000.000 tarefas e causará um StackOverflowException
. Se uma operação assíncrona for adicionada em cada chamada de loop, o código usará um heap essencialmente ilimitado. Considere mudar este código para usar um loop explícito, por exemplo:
let taskLoopGood (count: int) : Task<string> =
task {
for i in count .. 1 do
printfn $"looping... count = {count}"
return "done!"
}
let t = taskLoopGood 10000000
t.Wait()
Se forem necessárias chamadas tail assíncronas, use uma expressão assíncrona F#, que dá suporte a chamadas tail. Por exemplo:
let rec asyncLoopGood (count: int) =
async {
if count = 0 then
return "done!"
else
printfn $"looping..., count = {count}"
return! asyncLoopGood (count-1)
}
let t = asyncLoopGood 1000000 |> Async.StartAsTask
t.Wait()
Implementação da tarefa
As tarefas são implementadas usando Código retomável, um novo recurso do F# 6. As tarefas são compiladas em "Máquinas de Estado Retomáveis" pelo compilador F#. Eles são descritos em detalhes no RFC de Código Retomável e em uma sessão da comunidade do compilador F#.