Expresiones de tareas

En este artículo se describe la compatibilidad en F# con 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.

El código asincrónico se suele crear 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.

Syntax

task { expression }

En la sintaxis anterior, el cálculo representado por expression se configura 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 es el tipo devuelto por la expresión cuando se usa la palabra clave 'T return .

Enlace mediante let!

En una expresión de tarea, algunas expresiones y operaciones son sincrónicas y otras son asincrónicas. Cuando se espera el resultado de una operación asincrónica, en lugar de un enlace let normal, 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. Una vez que se devuelve el lado derecho let! del enlace, el resto de la tarea reanuda la ejecución.

El código siguiente muestra la diferencia entre let y let! . La línea de código que usa simplemente crea una tarea como un objeto que puede esperar más adelante let 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 an 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:

Expresiones return

Dentro de las expresiones de return expr tarea, se usa para devolver el resultado de una tarea.

Expresiones return!

Dentro de las expresiones de return! expr tarea, se usa para devolver el resultado de otra tarea. Equivale 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 , , , , y while .. do try .. with .. try .. finally .. if .. then .. else if .. then .. . A su vez, pueden incluir más construcciones de tareas, a excepción de los controladores with y , que se ejecutan finally sincrónicamente. Si necesita un try .. finally .. asincrónico, use un use enlace en combinación con un objeto de tipo IAsyncDisposable .

use enlaces use! y

Dentro de las expresiones de tarea, use los enlaces pueden enlazarse a valores de tipo IDisposable o IAsyncDisposable . En este último caso, la operación de limpieza de eliminación se ejecuta de forma asincrónica.

Además de let! , puede usar para realizar enlaces use! asincrónicos. La diferencia entre let! y es la misma que la diferencia entre y use! let use . Para use! , el objeto se elimina al cerrar el ámbito actual. Tenga en cuenta que en F# 6, no permite inicializar un valor en use! NULL, aunque use sí.

Tareas de valor

Las tareas de valor son structs 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>

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 implícitamente comprobaciones de cancelación. 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 piensa hacer que el código se pueda cancelar correctamente, compruebe cuidadosamente que pasa el token de cancelación a 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 de lectura asincrónica específica 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 sirvan como agentes intercalados y cooperativas que se ejecutan en un subproceso de interfaz de usuario sin bloquear la interfaz de usuario. Si no está presente, las continuaciones de tareas se programan en el grupo de subprocesos de .NET.

En la práctica, a menudo es deseable que el código de biblioteca que genera tareas ignore el contexto de sincronización y, en su lugar, siempre cambia al grupo de subprocesos de .NET, si es necesario. Esto se puede lograr mediante backgroundTask { } :

backgroundTask { expression }

Una tarea en segundo plano omite cualquiera en el siguiente sentido: si se inicia en un subproceso con un valor distinto de NULL, cambia a un subproceso en segundo plano en el grupo de subprocesos SynchronizationContext.Current SynchronizationContext.Current mediante Task.Run . Si se inicia en un subproceso con SynchronizationContext.Current null, 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 externo a una tarea en segundo plano se volverá a sincronizar con al SynchronizationContext.Current finalizar la tarea en segundo plano.

Limitaciones de las tareas con respecto a las llamadas finales

A diferencia de las expresiones asincrónicas de F#, las expresiones de tarea no admiten llamadas de cola. Es decir, cuando se ejecuta, la tarea actual se registra como en espera de la tarea return! cuyo resultado se devuelve. 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 pila o montón sin enlazar. Por ejemplo, considere el siguiente 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()

Este estilo de codificación no debe usarse con expresiones de tarea, creará una cadena de — 100 000 000 tareas y provocará una StackOverflowException . Si se agrega una operación asincrónica en cada invocación de bucle, el código usará un montón esencialmente sin enlazar. 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 = loopBad 10000000
t.Wait()

Si se requieren llamadas de cola asincrónicas, use una expresión asincrónica de F#, que admite las 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 = loop 1000000 |> Async.StartAsTask
t.Wait()

Implementación de tareas

Las tareas se implementan mediante Resumable Code, una nueva característica de F# 6. El compilador de F# compila las tareas en "Máquinas de estado reanudables". Se describen en detalle en el código reanudable RFCy en una sesión de la comunidad de compiladores de F#.

Vea también