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

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

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

Синтаксис

task { expression }

В предыдущем синтаксисе вычисления, представленные expression командой, настроены для выполнения в качестве задачи .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 { } могут ожидать следующих типов асинхронных операций:

  • Задачи Task<TResult> .NET и не универсальные Task.
  • Задачи ValueTask<TResult> значений .NET и не универсальные ValueTask.
  • Асинхронные Async<T>вычисления F# .
  • Любой объект после шаблона GetAwaiter, указанный в F# RFC FS-1097.

Выражения return

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

Выражения return!

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

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

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

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

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

Кроме того let!, можно использовать use! для асинхронных привязок. Разница между и совпадает с разницей между let!let и use.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()

Этот стиль программирования не должен использоваться с выражениями задач— он создаст цепочку задач 10000000 и приведет к возникновению 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()

Если требуются асинхронные хвостовые значения, используйте асинхронное выражение 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. Задачи компилируются в "Повторно обновляемые компьютеры состояний" компилятором F#. Они подробно описаны в коде RFC, а также в сеансе сообщества компилятора F#.

См. также