Nota
L'accés a aquesta pàgina requereix autorització. Podeu provar d'iniciar la sessió o de canviar els directoris.
L'accés a aquesta pàgina requereix autorització. Podeu provar de canviar els directoris.
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.
Canalizaciones de resultados y simultaneidad limitadas
Para limitar el número de operaciones que se inician a la vez, FSharp.Core 11 proporciona Task.parallelLimit. Pase generadores que aceptan tokens de cancelación, en lugar de tareas que ya se han iniciado:
open System.IO
open System.Threading
let readFiles (paths: seq<string>) (ct: CancellationToken) =
paths
|> Seq.map (fun path ->
fun token -> File.ReadAllTextAsync(path, token))
|> Task.parallelLimit 4 ct
La matriz de resultados conserva el orden de entrada. Un error solicita la cancelación del trabajo relacionado y cada operación debe observar su token.
Task.sequential ejecuta fábricas una a la vez; Task.parallelDoLimit y Task.sequentialDo devuelven unit en lugar de una matriz.
Para Task.maplas canalizaciones de resultados , Task.bindy Task.catch y los asistentes de Async/ValueTask coincidentes, consulte Funciones asincrónicas y de tareas.
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#.
Experimentos asincrónicos en tiempo de ejecución
Los autores del generador pueden experimentar con .NET sincronización en tiempo de ejecución. Normalmente, una máquina de estado generada por el compilador almacena el estado necesario para continuar después de una espera. Con el entorno de ejecución asincrónico, el tiempo de ejecución pausa y reanuda un método marcado especialmente en su lugar.
Nota:
Este prototipo usa una característica F# 11.2. En el SDK de .NET 11.0.100, habilite <LangVersion>preview</LangVersion> con un compilador que contenga compatibilidad asincrónica en tiempo de ejecución. También necesita ensamblados en tiempo de ejecución y de referencia que MethodImplOptions.Async exponen y System.Runtime.CompilerServices.AsyncHelpers.Await. Los ensamblados de referencia describen las API en tiempo de compilación; el tiempo de ejecución .NET debe implementar el comportamiento de pausa/reanudación.
La combinación de componentes comprobados del ejemplo es:
| Component | Versión o requisito |
|---|---|
| Compilador | El compilador F# 11 proporcionado por FSharp.Compiler.Service 43.13.101-rc2.26473.112. |
| FSharp.Core | Empaqueta , 11.0.101-rc2.26473.112mediante lib/net10.0/FSharp.Core.dll. Esta compilación proporciona StateMachineHelpers.__runtimeAsyncReturn; las netstandard2.0 compilaciones y netstandard2.1 no. |
| destino de Project |
net11.0. |
| Ensamblados en tiempo de ejecución y de referencia |
11.0.0-rc.1.26425.128.NET . |
Estos componentes probados no son una sola distribución del SDK. .NET 10.0.12 expone las API de referencia, pero se produce un error en la comprobación de pausa/reanudación en su entorno de ejecución con TypeLoadException. Use la combinación comprobada de tiempo de ejecución o referencia anterior; ni el modo de vista previa ni la presencia de API ofrecen compatibilidad con tiempo de ejecución.
El siguiente sitio de llamada usa un generador definido por la aplicación para leer un archivo de forma asincrónica:
let readManifest (path: string) =
runtimeTask {
let! text = System.IO.File.ReadAllTextAsync path
return text.Length
}
El generador conecta la expresión de cálculo a la función intrínseca de devolución asincrónica en tiempo de ejecución y al asistente await. No es un nuevo generador integrado task :
open System.Threading.Tasks
open System.Runtime.CompilerServices
open Microsoft.FSharp.Core.CompilerServices
type RuntimeTaskBuilder() =
member inline _.Delay([<InlineIfLambda>] code: unit -> 'T) = code
member inline _.Run([<InlineIfLambda>] code: unit -> 'T) : Task<'T> =
StateMachineHelpers.__runtimeAsyncReturn (code())
member inline _.Return(value: 'T) = value
member inline _.Bind(work: Task<'T>, [<InlineIfLambda>] next: 'T -> 'U) =
next (AsyncHelpers.Await work)
let runtimeTask = RuntimeTaskBuilder()
__runtimeAsyncReturn marca el método envolvente para el entorno de ejecución asincrónico. Dentro de ese método, AsyncHelpers.Await puede suspender el trabajo sin terminar y devolver el control al autor de la llamada y, a continuación, reanudar con el resultado. No es una llamada de bloqueo .Result .
El compilador comprueba los patrones de suspensión admitidos; byrefs no puede permanecer activo en una suspensión. El ejemplo solo define Delay, Run, Returny una Bind sobrecarga, no un generador de producción con cancelación, bucles o eliminación asincrónica.
Habilite la característica F# 11.2 donde se define y expande el generador insertado. Los autores de llamadas de una API ya compilada readManifest no lo necesitan. Las expresiones ordinarias task conservan su implementación. Use AsyncHelpers.Await solo en métodos asincrónicos en tiempo de ejecución; consulte la especificación asincrónica en tiempo de ejecución.
El proyecto de ejemplo completo incluye un punto de entrada de consola. Desde su directorio, ejecute dotnet run -- manifest.txt con herramientas compatibles y un archivo que se va a leer. Un archivo que contiene exactamente manifest, sin una nueva línea final, genera 8. El punto de entrada de la consola bloquea la finalización; readManifest el propio valor devuelve Task<int>.