Expressões de tarefas
Este artigo descreve o suporte em F# para expressões de tarefas, que são semelhantes a expressões assíncronas, mas permitem que você crie tarefas .NET diretamente. Como as expressões assíncronas, as expressões de tarefa executam código de forma assíncrona, ou seja, sem bloquear a execução de outros trabalhos.
O código assíncrono é normalmente criado usando expressões assíncronas. O uso de expressões de tarefa é preferível ao interoperar extensivamente com bibliotecas .NET que criam ou consomem tarefas .NET. As expressões de tarefas 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 mais adiante no artigo.
Sintaxe
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 return
palavra-chave é usada.
Vinculação usando let!
Em uma expressão de tarefa, algumas expressões e operações são síncronas e outras assíncronas. Quando você aguarda o resultado de uma operação assíncrona, em vez de uma ligação comum let
, você usa let!
. O efeito de let!
é permitir que a execução continue em outros cálculos ou threads à medida que a computação está sendo executada. Depois que o lado direito da let!
vinculação retorna, 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 F# task { }
podem aguardar os seguintes tipos de operações assíncronas:
- Tarefas Task<TResult> .NET e o arquivo não genérico Task.
- Tarefas ValueTask<TResult> de valor .NET e o arquivo não genérico ValueTask.
- Cálculos assíncronos
Async<T>
F# . - Qualquer objeto seguindo o padrão "GetAwaiter" especificado em F# RFC FS-1097.
return
expressões
Dentro de expressões de tarefa, return expr
é usado para retornar o resultado de uma tarefa.
return!
expressões
Dentro de expressões de tarefa, return! expr
é usado para retornar o resultado de outra tarefa. É equivalente a usar let!
e, em seguida, retornar imediatamente o resultado.
Fluxo de controlo
As expressões de tarefa podem incluir as construções for .. in .. do
de fluxo de controle , , while .. do
, try .. with ..
try .. finally ..
, if .. then .. else
e if .. then ..
. Estes, por sua vez, podem incluir outras construções de tarefas, exceto para os with
manipuladores e finally
, que são executados de forma síncrona. Se você precisar de um assíncrono try .. finally ..
, use uma use
associação em combinação com um objeto do tipo IAsyncDisposable
.
use
e use!
vinculações
Nas expressões de tarefa, use
as associações podem ser associadas a valores do tipo IDisposable ou IAsyncDisposable. Para este último, a operação de limpeza do 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 que a diferença 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, mesmo que use
o faça.
Tarefas de valor
As 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 transforma em uma tarefa real usando .AsTask()
o .
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>
Adicionar 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 executam implicitamente verificações de cancelamento. Se o 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 oferecem 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 poderá ser cancelada.
Tarefas em segundo plano
Por padrão, as tarefas do .NET são agendadas usando, se SynchronizationContext.Current presente. Isso permite que as tarefas sirvam 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 estiverem presentes, as continuações de tarefas serão agendadas para o pool de threads do .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
uma no seguinte sentido: se iniciada em um thread com não-nulo SynchronizationContext.Current
, ela alterna para um thread em segundo plano no pool de threads usando Task.Run
. Se iniciado em um thread com null SynchronizationContext.Current
, ele é executado nesse mesmo thread.
Nota
Na prática, isso significa que as chamadas para ConfigureAwait(false)
normalmente não são necessárias no código de tarefa F#. Em vez disso, as tarefas que se destinam a ser executadas em segundo plano devem ser criadas usando backgroundTask { ... }
o . Qualquer ligação de tarefa externa a uma tarefa em segundo plano será ressincronizada com a tarefa em segundo plano após a SynchronizationContext.Current
conclusão da tarefa.
Limitações de tarefas em relação a tailcalls
Ao contrário das expressões assíncronas do F#, as expressões de tarefa não suportam tailcalls. Ou seja, quando return!
é executada, a tarefa atual é registrada como aguardando a tarefa cujo resultado está sendo retornado. Isso significa que funções recursivas e métodos implementados usando expressões de tarefa podem criar cadeias ilimitadas de tarefas, e estas podem usar pilha ou pilha não limitada. 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 tarefas — ele criará uma cadeia de 10000000 tarefas e causará um StackOverflowException
arquivo . Se uma operação assíncrona for adicionada em cada invocação de loop, o código usará um heap essencialmente ilimitado. Considere alternar esse 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 tailcalls assíncronas, use uma expressão assíncrona F#, que suporta tailcalls. 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()
Execução da tarefa
As tarefas são implementadas usando o Código Retomável, um novo recurso no F# 6. As tarefas são compiladas em "Resumable State Machines" pelo compilador F#. Eles são descritos em detalhes no RFC do código Resumable e em uma sessão da comunidade do compilador F#.