Expresiones de tareas

En este artículo se describe la compatibilidad de F# con las expresiones de tarea, que son similares a las expresiones asincrónicas, pero que 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. Es preferible usar expresiones de tarea cuando se interopera mucho 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, una 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 palabra clave return.

Enlace mediante let!

En una expresión de tarea, algunas expresiones y operaciones son sincrónicas y otras 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 mientras se realiza el cálculo. Después de que se devuelva el lado derecho del enlace let!, 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 task { } de F# pueden esperar los siguientes tipos de operaciones asincrónicas:

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. 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, while .. do, try .. with .., try .. finally .., if .. then .. else y if .. then ... A su vez, pueden incluir construcciones de tareas adicionales, excepto los controladores with y finally, que se ejecutan de forma sincrónica. Si necesita una construcción asincrónica try .. finally .., use un enlace use en combinación con un objeto de tipo IAsyncDisposable.

Enlaces use y use!

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

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. En 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í.

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>

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 que el código se pueda cancelar correctamente, 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 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á presente. 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 se programan en el grupo 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 que, en su lugar, cambie siempre al grupo de subprocesos de .NET, si es necesario. Puede conseguirlo mediante backgroundTask { }:

backgroundTask { expression }

Una tarea en segundo plano omite cualquier SynchronizationContext.Current en el sentido siguiente: si se inicia en un subproceso con un valor distinto de NULL de SynchronizationContext.Current, cambia a un subproceso en segundo plano del grupo de subprocesos mediante Task.Run. Si se inicia en un subproceso con un valor NULL de SynchronizationContext.Current, se ejecuta en ese mismo subproceso.

Nota

En la práctica, 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 finalizar 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 tarea no admiten llamadas de cola. Es decir, cuando se ejecuta return!, la tarea actual se registra en espera de la tarea 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 una 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 se debe usar con expresiones de tarea; creará una cadena de 10 000 000 tareas y provocará una excepción StackOverflowException. Si se agrega una operación asincrónica en cada invocación de bucle, el código usará un montón sin enlazar esencialmente. 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 de 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 se compilan en "Máquinas de estado reanudable". Se describen con detalle en el RFC de código reanudable y en una sesión de la comunidad del compilador de F#.

Vea también