Observação
O acesso a essa página exige autorização. Você pode tentar entrar ou alterar diretórios.
O acesso a essa página exige autorização. Você pode tentar alterar os diretórios.
A programação assíncrona é um mecanismo essencial para aplicativos modernos por diversos motivos. Há dois casos de uso primários que a maioria dos desenvolvedores encontrará:
- Apresentar um processo de servidor que pode atender a um número significativo de solicitações de entrada simultâneas, minimizando os recursos do sistema ocupados enquanto o processamento de solicitações aguarda entradas de sistemas ou serviços externos a esse processo
- Manter uma interface do usuário responsiva ou um thread principal durante o progresso simultâneo do trabalho em segundo plano
Apesar de o trabalho em segundo plano, muitas vezes, envolver o uso de vários threads, é importante considerar os conceitos de assincronia e multithreading separadamente. Na verdade, são preocupações separadas, e uma não implica a outra. Este artigo descreve os conceitos separados com mais detalhes.
Assincronia definida
O ponto anterior - que a assincronia é independente da utilização de múltiplas threads - merece ser explicado um pouco mais. Às vezes, há três conceitos relacionados, mas estritamente independentes um do outro:
- Simultaneidade; quando vários cálculos são executados em períodos de tempo sobrepostos.
- Paralelismo; quando vários cálculos ou várias partes de uma única computação são executados exatamente ao mesmo tempo.
- Assíncrona; quando um ou mais cálculos podem ser executados separadamente do fluxo do programa principal.
Todos os três são conceitos ortogonais, mas podem ser facilmente confundidos, especialmente quando são usados juntos. Por exemplo, talvez seja necessário executar vários cálculos assíncronos em paralelo. Essa relação não significa que paralelismo ou assincronia impliquem um no outro.
Se você considerar a etimologia da palavra "assíncrona", há duas partes envolvidas:
- "a" significa "não".
- "síncrono", que significa "ao mesmo tempo".
Ao juntar esses dois termos, você verá que "assíncrono" significa "não ao mesmo tempo". É isso! Não há implicação de simultaneidade ou paralelismo nesta definição. Isso também é verdade na prática.
Em termos práticos, cálculos assíncronos em F# são agendados para serem executados independentemente do fluxo principal do programa. Essa execução independente não implica simultaneidade ou paralelismo, nem implica que uma computação sempre ocorre em segundo plano. Na verdade, cálculos assíncronos podem até mesmo ser executados de forma síncrona, dependendo da natureza da computação e do ambiente em que a computação está sendo executada.
A principal vantagem que você deve ter é que os cálculos assíncronos são independentes do fluxo principal do programa. Embora haja poucas garantias sobre quando ou como uma computação assíncrona é executada, há algumas abordagens para orquestrar e agendar. O restante deste artigo explora os principais conceitos de assincronismo no F# e como usar os tipos, funções e expressões integrados ao F#.
Conceitos fundamentais
No F#, a programação assíncrona é centralizada em torno de dois conceitos principais: cálculos assíncronos e tarefas.
- O tipo
Async<'T>
comasync { }
expressões , que representa uma computação assíncrona componível que pode ser iniciada para formar uma tarefa. - O tipo
Task<'T>
comtask { }
expressões, que representa uma tarefa .NET em execução.
Em geral, você deve considerar o uso task {…}
em vez de async {…}
em um novo código se estiver interoperando com bibliotecas .NET que usam tarefas e se você não depender de tailcalls de código assíncronas ou propagação implícita de token de cancelamento.
Principais conceitos de assíncrono
Você pode ver os conceitos básicos da programação "assíncrona" no exemplo a seguir:
open System
open System.IO
// Perform an asynchronous read of a file using 'async'
let printTotalFileBytesUsingAsync (path: string) =
async {
let! bytes = File.ReadAllBytesAsync(path) |> Async.AwaitTask
let fileName = Path.GetFileName(path)
printfn $"File {fileName} has %d{bytes.Length} bytes"
}
[<EntryPoint>]
let main argv =
printTotalFileBytesUsingAsync "path-to-file.txt"
|> Async.RunSynchronously
Console.Read() |> ignore
0
No exemplo, a printTotalFileBytesUsingAsync
função é do tipo string -> Async<unit>
. Chamar a função realmente não executa a computação assíncrona. Em vez disso, retorna um Async<unit>
que atua como uma especificação do trabalho que deve ser executado de forma assíncrona. Ele chama Async.AwaitTask
em seu corpo, o que converte o resultado em ReadAllBytesAsync para um tipo apropriado.
Outra linha importante é a chamada para Async.RunSynchronously
. Essa é uma das funções iniciais do módulo Assíncrono que você precisará chamar se quiser realmente executar uma computação assíncrona F#.
Essa é uma diferença fundamental com o estilo C#/Visual Basic de programação async
. Em F#, cálculos assíncronos podem ser considerados como tarefas frias. Elas devem ser claramente iniciadas para realmente ocorrer a execução. Isso tem algumas vantagens, pois permite combinar e sequenciar o trabalho assíncrono muito mais facilmente do que no C# ou no Visual Basic.
Combinar cálculos assíncronos
Aqui está um exemplo que se baseia no anterior combinando cálculos:
open System
open System.IO
let printTotalFileBytes path =
async {
let! bytes = File.ReadAllBytesAsync(path) |> Async.AwaitTask
let fileName = Path.GetFileName(path)
printfn $"File {fileName} has %d{bytes.Length} bytes"
}
[<EntryPoint>]
let main argv =
argv
|> Seq.map printTotalFileBytes
|> Async.Parallel
|> Async.Ignore
|> Async.RunSynchronously
0
Como você pode ver, a main
função tem mais alguns elementos. Conceitualmente, ele faz o seguinte:
- Transforma os argumentos de linha de comando em uma sequência de computações
Async<unit>
comSeq.map
. - Cria um
Async<'T[]>
que programa e executa as computaçõesprintTotalFileBytes
em paralelo quando for executado. - Crie um
Async<unit>
que executará a computação paralela e ignorará seu resultado (que é umunit[]
). - Realize explicitamente o cálculo total composto com
Async.RunSynchronously
, bloqueando até que ele seja concluído.
Quando este programa é executado, printTotalFileBytes
é executado em paralelo para cada argumento de linha de comando. Como os cálculos assíncronos são executados independentemente do fluxo do programa, não há nenhuma ordem definida na qual eles imprimem suas informações e terminam de executar. Os cálculos serão agendados em paralelo, mas sua ordem de execução não é garantida.
Computações assíncronas de sequência
Como Async<'T>
é uma especificação de trabalho em vez de uma tarefa já em execução, você pode executar transformações mais complexas facilmente. Aqui está um exemplo que sequencia um conjunto de cálculos assíncronos para que eles executem um após o outro.
let printTotalFileBytes path =
async {
let! bytes = File.ReadAllBytesAsync(path) |> Async.AwaitTask
let fileName = Path.GetFileName(path)
printfn $"File {fileName} has %d{bytes.Length} bytes"
}
[<EntryPoint>]
let main argv =
argv
|> Seq.map printTotalFileBytes
|> Async.Sequential
|> Async.Ignore
|> Async.RunSynchronously
|> ignore
Isso agendará printTotalFileBytes
para ser executado na ordem dos elementos de argv
em vez de agendá-los em paralelo. Como cada operação sucessiva não será agendada até que a computação anterior tenha terminado de ser executada, os cálculos são sequenciados de modo que não haja sobreposição em sua execução.
Funções importantes do módulo Async
Ao escrever código assíncrono em F#, você geralmente interagirá com uma estrutura que manipula o agendamento de cálculos para você. No entanto, esse nem sempre é o caso, portanto, é bom entender as várias funções que podem ser usadas para agendar o trabalho assíncrono.
Como as computações assíncronas F# são uma especificação de trabalho em vez de uma representação de trabalho que já está em execução, elas devem ser iniciadas explicitamente com uma função inicial. Há muitos métodos iniciais assíncronos que são úteis em contextos diferentes. A seção a seguir descreve algumas das funções iniciais mais comuns.
Async.StartChild
Inicia uma computação filho em uma computação assíncrona. Isso permite que vários cálculos assíncronos sejam executados simultaneamente. A computação filho compartilha um token de cancelamento com a computação pai. Se a computação pai for cancelada, a computação filho também será cancelada.
Assinatura:
computation: Async<'T> * ?millisecondsTimeout: int -> Async<Async<'T>>
Quando usar:
- Quando você deseja executar várias computações assíncronas simultaneamente em vez de uma de cada vez, mas não as tem agendadas em paralelo.
- Quando você deseja vincular o tempo de vida de uma computação filho à de uma computação pai.
Com o que tomar cuidado:
- Iniciar vários cálculos com
Async.StartChild
não é o mesmo que agendá-los em paralelo. Se você quiser agendar cálculos em paralelo, useAsync.Parallel
. - Cancelar uma computação pai aciona o cancelamento de todos os cálculos filho iniciados.
Async.StartImmediate
Executa uma computação assíncrona, começando imediatamente no thread atual do sistema operacional. Isso será útil se você precisar atualizar algo no thread de chamada durante a computação. Por exemplo, se uma computação assíncrona precisar atualizar uma interface do usuário (como atualizar uma barra de progresso), deverá Async.StartImmediate
ser usada.
Assinatura:
computation: Async<unit> * ?cancellationToken: CancellationToken -> unit
Quando usar:
- Quando você precisa atualizar algo no thread de chamada no meio de uma computação assíncrona.
Com o que tomar cuidado:
- O código na computação assíncrona é executado em qualquer thread em que um deles esteja programado. Isso pode ser problemático se esse thread for de alguma forma sensível, como um thread de interface do usuário. Nesses casos,
Async.StartImmediate
é provável que seja inadequado usar.
Async.StartAsTask
Executa uma computação no pool de threads. Retorna um Task<TResult> que será concluído no estado correspondente depois que a computação for encerrada (produz resultado, gera exceção ou é cancelada). Se nenhum token de cancelamento for fornecido, o token de cancelamento padrão será usado.
Assinatura:
computation: Async<'T> * ?taskCreationOptions: TaskCreationOptions * ?cancellationToken: CancellationToken -> Task<'T>
Quando usar:
- Quando você precisa chamar uma API .NET que produz um Task<TResult> para representar o resultado de uma computação assíncrona.
Com o que tomar cuidado:
- Essa chamada alocará um objeto adicional
Task
, o que poderá aumentar a sobrecarga se ele for usado com frequência.
Async.Parallel
Agenda uma sequência de cálculos assíncronos a serem executados em paralelo, gerando uma matriz de resultados na ordem em que foram fornecidos. O grau de paralelismo pode ser ajustado/limitado opcionalmente especificando o maxDegreeOfParallelism
parâmetro.
Assinatura:
computations: seq<Async<'T>> * ?maxDegreeOfParallelism: int -> Async<'T[]>
Quando usá-lo:
- Se você precisar executar um conjunto de cálculos ao mesmo tempo e não tiver nenhuma dependência da ordem de execução deles.
- Se você não precisar de resultados de computações agendadas em paralelo até que todas tenham sido concluídas.
Com o que tomar cuidado:
- Você só pode acessar a matriz resultante de valores depois que todos os cálculos tiverem sido concluídos.
- As computações serão executadas sempre que acabarem sendo programadas. Esse comportamento significa que você não pode confiar na ordem de execução deles.
Async.Sequential
Agenda uma sequência de cálculos assíncronos a serem executados na ordem em que são passados. A primeira computação será executada, depois a próxima e assim por diante. Nenhum cálculo será executado em paralelo.
Assinatura:
computations: seq<Async<'T>> -> Async<'T[]>
Quando usá-lo:
- Se você precisar executar várias computações na ordem.
Com o que tomar cuidado:
- Você só pode acessar a matriz resultante de valores depois que todos os cálculos tiverem sido concluídos.
- Os cálculos serão executados na ordem em que forem passados para essa função, o que pode significar que mais tempo será decorrido antes que os resultados sejam retornados.
Async.AwaitTask
Retorna uma computação assíncrona que aguarda a conclusão do determinado Task<TResult> e retorna seu resultado como um Async<'T>
Assinatura:
task: Task<'T> -> Async<'T>
Quando usar:
- Quando você estiver usando uma API do .NET que retorna uma Task<TResult> na computação assíncrona F#.
Com o que tomar cuidado:
- As exceções são encapsuladas em AggregateException seguindo a convenção da Biblioteca Paralela de Tarefas; esse comportamento é diferente de como F# assíncrono geralmente apresenta exceções.
Async.Catch
Cria uma computação assíncrona que executa um determinado Async<'T>
, retornando um Async<Choice<'T, exn>>
. Se o determinado Async<'T>
for concluído com êxito, um Choice1Of2
será retornado com o valor resultante. Se uma exceção for gerada antes de ser concluída, uma Choice2of2
será retornada com a exceção gerada. Se ele for usado em uma computação assíncrona que é composta por muitos cálculos e um desses cálculos gera uma exceção, a computação abrangente será totalmente interrompida.
Assinatura:
computation: Async<'T> -> Async<Choice<'T, exn>>
Quando usar:
- Quando você está executando um trabalho assíncrono que pode não ocorrer com uma exceção e deseja lidar com essa exceção no chamador.
Com o que tomar cuidado:
- Ao usar computações assíncronas combinadas ou sequenciadas, a computação abrangente será totalmente interrompida se uma de suas computações "internas" gerar uma exceção.
Async.Ignore
Cria uma computação assíncrona que executa a computação fornecida, mas descarta seu resultado.
Assinatura:
computation: Async<'T> -> Async<unit>
Quando usar:
- Quando você tem uma computação assíncrona cujo resultado não é necessário. Isso é análogo à
ignore
função para código não assíncrono.
Com o que tomar cuidado:
- Se você precisar usar
Async.Ignore
porque deseja usarAsync.Start
ou outra função que exijaAsync<unit>
, considere se descartar o resultado está bem. Evite descartar resultados apenas para ajustar uma assinatura de tipo.
Async.RunSynchronously
Executa uma computação assíncrona e aguarda o resultado no thread de chamada. Propaga uma exceção caso a computação produza uma. A chamada é bloqueada.
Assinatura:
computation: Async<'T> * ?timeout: int * ?cancellationToken: CancellationToken -> 'T
Quando usá-lo:
- Se precisar, use apenas uma vez em um aplicativo: no ponto de entrada de um arquivo executável.
- Quando você não se importa com o desempenho e deseja executar um conjunto de outras operações assíncronas de uma só vez.
Com o que tomar cuidado:
- A chamada
Async.RunSynchronously
bloqueia o thread de chamada até que a execução seja concluída.
Async.Start
Inicia uma computação assíncrona que retorna unit
no pool de threads. Não aguarda a conclusão e/ou observa um resultado de exceção. As computações aninhadas e iniciadas com Async.Start
são feitas independentemente da computação pai chamada. Seu tempo de vida não está vinculado a nenhuma computação pai. Se a computação pai for cancelada, nenhuma computação filho será cancelada.
Assinatura:
computation: Async<unit> * ?cancellationToken: CancellationToken -> unit
Use somente quando:
- Você tem uma computação assíncrona que não gera um resultado e/ou requer processamento de um.
- Você não precisa saber quando uma computação assíncrona é concluída.
- Você não se importa em qual thread uma computação assíncrona é executada.
- Você não precisa estar ciente ou relatar exceções resultantes da execução.
Com o que tomar cuidado:
- As exceções geradas por cálculos que começam com
Async.Start
não são propagadas para o chamador. A pilha de chamadas será completamente desfeita. - Qualquer trabalho (como chamar
printfn
) iniciado comAsync.Start
não fará com que o efeito ocorra na thread principal da execução de um programa.
Interoperar com o .NET
Se estiver usando async { }
programação, talvez seja necessário interoperar com uma biblioteca .NET ou uma base de código C# que use programação assíncrona no estilo async/await. Como o C# e a maioria das bibliotecas do .NET usam os tipos Task<TResult> e Task como suas abstrações principais, isso pode alterar a forma como você escreve seu código assíncrono em F#.
Uma opção é alternar para escrever tarefas do .NET diretamente usando task { }
. Como alternativa, você pode usar a Async.AwaitTask
função para aguardar uma computação assíncrona do .NET:
let getValueFromLibrary param =
async {
let! value = DotNetLibrary.GetValueAsync param |> Async.AwaitTask
return value
}
Você pode usar a Async.StartAsTask
função para passar uma computação assíncrona para um chamador do .NET:
let computationForCaller param =
async {
let! result = getAsyncResult param
return result
} |> Async.StartAsTask
Para trabalhar com APIs que usam Task (ou seja, cálculos assíncronos do .NET que não retornam um valor), talvez seja necessário adicionar uma função adicional que converterá uma Async<'T>
em um Task:
module Async =
// Async<unit> -> Task
let startTaskFromAsyncUnit (comp: Async<unit>) =
Async.StartAsTask comp :> Task
Já existe um Async.AwaitTask
que aceita um Task como entrada. Com essa e a função definida startTaskFromAsyncUnit
anteriormente, você pode iniciar e aguardar tipos Task de uma computação assíncrona F#.
Escrevendo tarefas do .NET diretamente em F#
No F#, você pode escrever tarefas diretamente usando task { }
, por exemplo:
open System
open System.IO
/// Perform an asynchronous read of a file using 'task'
let printTotalFileBytesUsingTasks (path: string) =
task {
let! bytes = File.ReadAllBytesAsync(path)
let fileName = Path.GetFileName(path)
printfn $"File {fileName} has %d{bytes.Length} bytes"
}
[<EntryPoint>]
let main argv =
let task = printTotalFileBytesUsingTasks "path-to-file.txt"
task.Wait()
Console.Read() |> ignore
0
No exemplo, a printTotalFileBytesUsingTasks
função é do tipo string -> Task<unit>
. A execução da tarefa começa ao chamar a função.
A chamada para task.Wait()
aguarda a conclusão da tarefa.
Relação com multithreading
Embora o threading seja mencionado ao longo deste artigo, há duas coisas importantes a serem lembradas:
- Não há afinidade entre uma computação assíncrona e um thread, a menos que seja iniciada explicitamente no thread atual.
- A programação assíncrona em F# não é uma abstração para multithreading.
Por exemplo, uma computação pode realmente ser executada no thread do chamador, dependendo da natureza do trabalho. Uma computação também pode "saltar" entre threads, emprestando por um pequeno período de tempo para fazer um trabalho útil entre períodos de "espera" (quando uma chamada de rede está em trânsito).
Embora o F# forneça algumas habilidades para iniciar uma computação assíncrona no thread atual (ou explicitamente não no thread atual), a assincronia geralmente não está associada a uma estratégia de threading específica.