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