Nota
O acceso a esta páxina require autorización. Pode tentar iniciar sesión ou modificar os directorios.
O acceso a esta páxina require autorización. Pode tentar modificar os directorios.
En este artículo se describe la compatibilidad con F# para expresiones de tarea, que son similares a las expresiones asincrónicas , pero permiten crear tareas de .NET directamente. Al igual que las expresiones asincrónicas, las expresiones de tarea ejecutan código de forma asincrónica, es decir, sin bloquear la ejecución de otro trabajo.
Normalmente, el código asincrónico se crea mediante expresiones asincrónicas. Se prefiere usar expresiones de tarea al interoperar ampliamente con bibliotecas de .NET que crean o consumen tareas de .NET. Las expresiones de tarea también pueden mejorar el rendimiento y la experiencia de depuración. Sin embargo, las expresiones de tarea incluyen algunas limitaciones, que se describen más adelante en el artículo.
Sintaxis
task { expression }
En la sintaxis anterior, el cálculo representado por expression
está configurado para ejecutarse como una tarea de .NET. La tarea se inicia inmediatamente después de ejecutar este código y se ejecuta en el subproceso actual hasta que se realiza su primera operación asincrónica (por ejemplo, una suspensión asincrónica, E/S asincrónica u otra operación asincrónica primitiva). El tipo de la expresión es Task<'T>
, donde 'T
es el tipo devuelto por la expresión cuando se usa la return
palabra clave .
Vinculación mediante let!
En una expresión de tarea, algunas expresiones y operaciones son sincrónicas y algunas son asincrónicas. Cuando espera el resultado de una operación asincrónica, en lugar de un enlace normal let
, se usa let!
. El efecto de let!
es permitir que la ejecución continúe en otros cálculos o subprocesos a medida que se realiza el cálculo. Después de que la parte derecha del let!
enlace regrese, el resto de la tarea reanuda la ejecución.
En el código siguiente se muestra la diferencia entre let
y let!
. La línea de código que usa let
simplemente crea una tarea como un objeto que puede esperar más adelante mediante, por ejemplo, task.Wait()
o task.Result
. La línea de código que usa let!
inicia la tarea y espera su 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)
Las expresiones de F# task { }
pueden esperar los siguientes tipos de operaciones asincrónicas:
- Tareas de .NET, Task<TResult> y las no genéricas Task.
- Tareas de valor de .NET ValueTask<TResult> y la no genérica ValueTask.
- Cálculos asincrónicos de F#
Async<T>
. - Cualquier objeto que siga el patrón "GetAwaiter" especificado en F# RFC FS-1097.
Expresiones return
Dentro de las expresiones de tarea, return expr
se usa para devolver el resultado de una tarea.
Expresiones return!
Dentro de las expresiones de tarea, return! expr
se usa para devolver el resultado de otra tarea. Es equivalente a usar let!
y, a continuación, devolver inmediatamente el resultado.
Flujo de control
Las expresiones de tarea pueden incluir las construcciones de flujo de control for .. in .. do
, while .. do
, try .. with ..
, try .. finally ..
, if .. then .. else
y if .. then ..
. A su vez, pueden incluir construcciones de tareas adicionales, excepto para los manejadores with
y finally
, que se ejecutan sincrónicamente. Si necesita un try .. finally ..
asincrónico, use un use
en combinación con un objeto de tipo IAsyncDisposable
.
enlaces use
y use!
Dentro de las expresiones de tarea, use
los enlazamientos pueden vincularse a valores de tipo IDisposable o IAsyncDisposable. Para este último, la operación de limpieza de desechos se ejecuta de forma asíncrona.
Además de let!
, puede usar use!
para realizar enlaces asincrónicos. La diferencia entre let!
y use!
es la misma que la diferencia entre let
y use
. Para use!
, el objeto se elimina al cerrar el ámbito actual. Tenga en cuenta que, en F# 6, use!
no permite inicializar un valor en null, aunque use
sí.
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
}
Tareas de valor
Las tareas de valor son estructuras que se usan para evitar asignaciones en la programación basada en tareas. Una tarea de valor es un valor efímero que se convierte en una tarea real mediante .AsTask()
.
Para crear una tarea de valor a partir de una expresión de tarea, use |> ValueTask<ReturnType>
o |> ValueTask
. Por ejemplo:
let makeTask() =
task { return 1 }
makeTask() |> ValueTask<int>
and!
vinculaciones (a partir de F# 10)
Dentro de las expresiones de tarea, es posible esperar simultáneamente para varias operaciones asincrónicas (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
}
Adición de tokens de cancelación y comprobaciones de cancelación
A diferencia de las expresiones asincrónicas de F#, las expresiones de tarea no pasan implícitamente un token de cancelación y no realizan comprobaciones de cancelación implícitamente. Si el código requiere un token de cancelación, debe especificar el token de cancelación como parámetro. Por ejemplo:
open System.Threading
let someTaskCode (cancellationToken: CancellationToken) =
task {
cancellationToken.ThrowIfCancellationRequested()
printfn $"continuing..."
}
Si tiene previsto cancelar correctamente el código, compruebe cuidadosamente que pasa el token de cancelación a través de todas las operaciones de biblioteca de .NET que admiten la cancelación. Por ejemplo, Stream.ReadAsync
tiene varias sobrecargas, una de las cuales acepta un token de cancelación. Si no usa esta sobrecarga, esa operación específica de lectura asincrónica no se podrá cancelar.
Tareas en segundo plano
De forma predeterminada, las tareas de .NET se programan mediante SynchronizationContext.Current si están presentes. Esto permite que las tareas actúen como agentes cooperativos intercalados que se ejecutan en un subproceso de interfaz de usuario sin bloquear la interfaz de usuario. Si no está presente, las continuaciones de tareas serán programadas en el pool de subprocesos de .NET.
En la práctica, a menudo es conveniente que el código de biblioteca que genera tareas omita el contexto de sincronización y, en su lugar, cambie siempre al grupo de subprocesos de .NET, si es necesario. Puede lograrlo mediante backgroundTask { }
:
backgroundTask { expression }
Una tarea en segundo plano omite cualquier SynchronizationContext.Current
en el siguiente sentido: si se inicia en un subproceso con un valor no nulo SynchronizationContext.Current
, cambia a un subproceso del grupo de subprocesos de en segundo plano mediante Task.Run
. Si se inicia en un subproceso con null SynchronizationContext.Current
, se ejecuta en ese mismo subproceso.
Nota:
En la práctica, esto significa que las llamadas a ConfigureAwait(false)
no suelen ser necesarias en el código de tarea de F#. En su lugar, las tareas diseñadas para ejecutarse en segundo plano deben crearse mediante backgroundTask { ... }
. Cualquier enlace de tarea externa a una tarea en segundo plano se resincronizará con SynchronizationContext.Current
al completarse la tarea en segundo plano.
Limitaciones de las tareas relacionadas con las llamadas de cola
A diferencia de las expresiones asincrónicas de F#, las expresiones de tareas no admiten tail calls. Es decir, cuando return!
se ejecuta, la tarea actual se registra como estando a la espera de la tarea cuyo resultado se está devolviendo. Esto significa que las funciones recursivas y los métodos implementados mediante expresiones de tarea pueden crear cadenas de tareas sin enlazar y pueden usar una pila o montón sin enlazar. Por ejemplo, considere el código siguiente:
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()
Este estilo de codificación no se debe usar con expresiones de tarea; creará una cadena de 10000000 tareas y provocará un StackOverflowException
. Si se agrega una operación asincrónica en cada invocación del bucle, el código usará un heap esencialmente sin límites. Considere la posibilidad de cambiar este código para usar un bucle explícito, por ejemplo:
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()
Si se requieren llamadas de cola asincrónicas, use una expresión asincrónica en F#, que admite llamadas de cola. Por ejemplo:
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()
Implementación de tareas
Las tareas se implementan mediante código reanudable, una nueva característica en F# 6. El compilador de F# compila las tareas en "Resumable State Machines". Estos se describen en detalle en el RFC de código resumible y en una sesión de la comunidad del compilador de F#.