Выражения задач

В этой статье описывается поддержка в F# для выражений задач, которые похожи на асинхронные выражения , но позволяют создавать задачи .NET напрямую. Как и асинхронные выражения, выражения задач выполняют код асинхронно, то есть без блокировки выполнения других работ.

Асинхронный код обычно создается с помощью асинхронных выражений. Использование выражений задач рекомендуется использовать при активном взаимодействии с библиотеками .NET, которые создают или используют задачи .NET. Выражения задач также могут повысить производительность и возможности отладки. Однако выражения задач поставляются с некоторыми ограничениями, которые описаны далее в статье.

Синтаксис

task { expression }

В предыдущем синтаксисе вычисление, представленное expression в качестве задачи .NET, настроено для выполнения в качестве задачи .NET. Задача запускается сразу после выполнения этого кода и выполняется в текущем потоке, пока не будет выполнена первая асинхронная операция (например, асинхронный спящий режим, асинхронный ввод-вывод или другая примитивная асинхронная операция). Тип выражения — Task<'T>это 'T тип, возвращаемый выражением при использовании ключевого return слова.

Привязка с помощью let!

В выражении задачи некоторые выражения и операции являются синхронными, а некоторые — асинхронными. При ожидании результата асинхронной операции вместо обычной let привязки используется let!. Эффект let! заключается в том, чтобы выполнение продолжалось на других вычислениях или потоках по мере выполнения вычислений. После возвращения правой части привязки let! остальная часть задачи возобновляет выполнение.

В следующем коде показано различие между let и let!. Строка кода, использующая let только создание задачи в качестве объекта, который можно ожидать позже с помощью, например task.Wait() , или task.Result. Строка кода, использующая let! запуск задачи и ожидающая ее результата.

// 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)

Выражения F# task { } могут ожидать следующих типов асинхронных операций:

Выражения return

В выражениях return expr задач используется для возврата результата задачи.

Выражения return!

В выражениях return! expr задач используется для возврата результата другой задачи. Он эквивалентен использованию let! , а затем немедленно возвращает результат.

Поток управления

Выражения задач могут включать конструкции for .. in .. doпотока управления, while .. do, , , try .. with .., try .. finally ..if .. then .. elseи if .. then ... Они, в свою очередь, могут включать дополнительные конструкции задач, за исключением with обработчиков, finally которые выполняются синхронно. Если требуется асинхронный try .. finally .., используйте привязку use в сочетании с объектом типа IAsyncDisposable.

use и use! привязки

В выражениях use задач привязки могут привязываться к значениям типа IDisposable или IAsyncDisposable. В последнем случае операция очистки удаления выполняется асинхронно.

Кроме того let!, можно использовать для use! выполнения асинхронных привязок. Разница между и use! совпадает с let! разницей между let и use. Для use!этого объект удаляется в конце текущей области. Обратите внимание, use! что в F# 6 не позволяет инициализировать значение null, даже если use это делается.

Задачи значения

Задачи значений — это структуры, используемые для предотвращения выделения ресурсов в программировании на основе задач. Задача "Значение" — это временное значение, которое превращается в реальную задачу с помощью .AsTask().

Чтобы создать задачу значения из выражения задачи, используйте |> ValueTask<ReturnType> или |> ValueTask. Например:

let makeTask() =
    task { return 1 }

makeTask() |> ValueTask<int>

Добавление маркеров отмены и проверок отмены

В отличие от асинхронных выражений F# выражения задач не неявно передают токен отмены и неявно не выполняют проверки отмены. Если для кода требуется маркер отмены, необходимо указать маркер отмены в качестве параметра. Например:

open System.Threading

let someTaskCode (cancellationToken: CancellationToken) =
    task {
        cancellationToken.ThrowIfCancellationRequested()
        printfn $"continuing..."
    }

Если вы планируете правильно отменить код, тщательно проверьте, передаете ли маркер отмены всем операциям библиотеки .NET, поддерживающим отмену. Например, Stream.ReadAsync имеет несколько перегрузок, одна из которых принимает токен отмены. Если вы не используете эту перегрузку, эта конкретная асинхронная операция чтения не будет отменена.

Фоновые задачи

По умолчанию задачи .NET планируются при SynchronizationContext.Current наличии. Это позволяет задачам выступать в качестве совместных, чередующихся агентов, выполняемых в потоке пользовательского интерфейса, не блокируя пользовательский интерфейс. В противном случае продолжение задач планируется в пул потоков .NET.

На практике часто желательно, чтобы код библиотеки, создающий задачи, игнорирует контекст синхронизации и при необходимости всегда переключается в пул потоков .NET. Это можно сделать с помощью backgroundTask { }:

backgroundTask { expression }

Фоновая задача игнорирует любую SynchronizationContext.Current из следующих параметров: если она запущена в потоке с ненулевым SynchronizationContext.Currentзначением, она переключается на фоновый поток в пуле потоков с помощью Task.Run. Если он запущен в потоке со значением NULL SynchronizationContext.Current, он выполняется в том же потоке.

Примечание

На практике это означает, что вызовы ConfigureAwait(false) обычно не требуются в коде задачи F#. Вместо этого задачи, предназначенные для выполнения в фоновом режиме, следует создавать с помощью backgroundTask { ... }. Любая внешняя привязка задачи к фоновой задаче будет повторно синхронизироваться с SynchronizationContext.Current завершением фоновой задачи.

Ограничения задач, связанных с tailcalls

В отличие от асинхронных выражений F#, выражения задач не поддерживают tailcalls. То есть при return! выполнении текущая задача регистрируется как ожидающая задачи, результат которой возвращается. Это означает, что рекурсивные функции и методы, реализованные с помощью выражений задач, могут создавать несвязанные цепочки задач, и они могут использовать несвязанный стек или кучу. Рассмотрим следующий пример кода:

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()

Этот стиль программирования не следует использовать с выражениями задач— он создаст цепочку из 1000000 задач и вызовет .StackOverflowException Если в каждом вызове цикла добавляется асинхронная операция, код будет использовать по существу несвязанную кучу. Рассмотрите возможность переключения этого кода для использования явного цикла, например:

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()

Если требуются асинхронные tailcalls, используйте асинхронное выражение F#, которое поддерживает tailcalls. Например:

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()

Реализация задачи

Задачи реализуются с помощью возобновляемого кода, новой функции в F# 6. Задачи компилируются в "Resumable State Machines" компилятором F#. Они подробно описаны в rfC для возобновления кода и в сеансе сообщества компилятора F#.

См. также