Compartilhar via


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 criar tarefas do .NET diretamente. Como 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.

Normalmente, o código assíncrono é criado usando expressões assíncronas. O uso de expressões de tarefa é preferencial ao interoperar extensivamente com bibliotecas .NET que criam ou consomem tarefas do .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 posteriormente no artigo.

Sintaxe

task { expression }

Na sintaxe anterior, a computação representada por expression está configurada para ser executada como uma tarefa .NET. A tarefa é iniciada imediatamente depois que esse código é executado 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 está 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 são assíncronas. Ao aguardar o resultado de uma operação assíncrona, em vez de uma associação comum let , você usa let!. O efeito de let! é permitir que a execução continue em outras computações ou threads enquanto a computação está sendo realizada. Depois que o lado direito da let! associação retornar, o restante da tarefa retomará 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:

Expressões return

Dentro de expressões de tarefa, return expr é usado para retornar o resultado de uma tarefa.

Expressões return!

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 controle

As expressões de tarefa podem incluir os constructos for .. in .. dode fluxo de controle, while .. do, try .. with .., , try .. finally ..e if .. then .. elseif .. then ... Eles podem, por sua vez, incluir outros constructos de tarefa, exceto os manipuladores with 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! associações

Dentro de expressões de tarefa, use ligações podem ligar-se a valores de tipo IDisposable ou IAsyncDisposable. Para este último, a operação de descarte e limpeza é executada de forma assíncrona.

Além disso 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 é liberado no término do escopo atual. Observe que, em F# 6, use! não permite que um valor seja inicializado como nulo, mesmo que use sim.

open System
open System.IO
open System.Security.Cryptography
task {
    // use IDisposable
    use httpClient = new Net.Http.HttpClient()
    // use! Task<IDisposable>
    use! exampleDomain = httpClient.GetAsync "https://example.com/data.enc"
   
    // use IDisposable
    use aes = Aes.Create()
    aes.KeySize <- 256
    aes.GenerateIV()
    aes.GenerateKey()
    // do! Task
    do! File.WriteAllTextAsync("key.iv.txt", $"Key: {Convert.ToBase64String aes.Key}\nIV: {Convert.ToBase64String aes.IV}")

    // use IAsyncDisposable
    use outputStream = File.Create "secret.enc"
    // use IDisposable
    use encryptor = aes.CreateEncryptor()
    // use IAsyncDisposable
    use cryptoStream = new CryptoStream(outputStream, encryptor, CryptoStreamMode.Write)
    // do! Task
    do! exampleDomain.Content.CopyToAsync cryptoStream
}

Tarefas de valor

Tarefas de valor são structs usados para evitar alocações na programação baseada em tarefas. Uma tarefa de valor é um valor efêmero que é transformado 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>

and! associações (a partir de F# 10)

Dentro de expressões de tarefa, é possível aguardar várias operações assíncronas simultaneamente (Task<'T>, ValueTask<'T>, Async<'T>, etc). Comparar:

// We'll wait for x to resolve and then for y to resolve. Overall execution time is sum of two execution times.
let getResultsSequentially() =
    task {
        let! x = getX()
        let! y = getY()
        return x, y
    }

// x and y will be awaited concurrently. Overall execution time is the time of the slowest operation.
let getResultsConcurrently() =
    task {
        let! x = getX()
        and! y = getY()
        return x, y
    }

Adicionando tokens de cancelamento e verificações de cancelamento

Ao contrário das expressões assíncronas F#, as expressões de tarefa não passam implicitamente um token de cancelamento e não executam implicitamente as verificações de cancelamento. Se o código exigir um token de cancelamento, você deverá 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 de biblioteca do .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 do .NET são agendadas usando SynchronizationContext.Current se estiverem presentes. Isso permite que as tarefas sirvam como agentes intercalados e cooperativos 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 tarefa serão agendadas para o pool de threads do .NET.

Na prática, geralmente é desejável que o código de biblioteca que gera tarefas ignore o contexto de sincronização e sempre mude para o pool de threads do .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 encadeamento com SynchronizationContext.Current não nulo, alterna para um encadeamento em segundo plano no pool de encadeamentos 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 chamadas para ConfigureAwait(false) normalmente não são necessárias no código de tarefas F#. Em vez disso, as tarefas destinadas a serem executadas em segundo plano devem ser criadas usando backgroundTask { ... }. Qualquer associação de tarefa externa a uma tarefa em segundo plano será ressincronizada ao término da tarefa em segundo plano.

Limitações de tarefas relacionadas a tailcalls

Ao contrário das expressões assíncronas do F#, as expressões de tarefa não dão suporte a tailcalls. Ou seja, quando return! é executada, a tarefa atual é registrada como aguardando a tarefa cujo resultado está sendo retornado. Isso implica que funções recursivas e métodos implementados usando expressões de tarefa podem criar cadeias de tarefas ilimitadas, e 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 10000000 tarefas e causará uma StackOverflowException. Se uma operação assíncrona for adicionada em cada iteração do 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 chamadas de retorno de cauda assíncronas forem necessárias, utilize uma expressão assíncrona em F#, que oferece suporte a essas chamadas. 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 o Código Resumável, um novo recurso em 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#.

Consulte também