Примечание.
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
В этой статье описывается поддержка в 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.
- Value-задачи .NET, ValueTask<TResult>, и необобщённые ValueTask.
- Асинхронные вычисления F#
Async<T>. - Любой объект, соответствующий шаблону 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 ... В свою очередь, они могут включать дополнительные конструкции задач, за исключением обработчиков with и finally, которые выполняются синхронно. Если вам нужен асинхронный try .. finally .., используйте use привязку в сочетании с объектом типа IAsyncDisposable.
привязки use и use!
В выражениях задач привязки use могут привязаться к значениям типа IDisposable или IAsyncDisposable. Для последнего операция очистки удаления выполняется асинхронно.
Помимо let!можно использовать use! для выполнения асинхронных привязок. Разница между let! и use! совпадает с разницей между let и use. Для use!объект удаляется при закрытии текущей области. Обратите внимание, что в F# 6 use! не позволяет инициализировать значение как null, даже если use это делает.
open System
open System.IO
open System.Security.Cryptography
task {
// use IDisposable
use httpClient = new Net.Http.HttpClient()
// use! Task<IDisposable>
use! exampleDomain = httpClient.GetAsync "https://example.com/data.enc"
// use IDisposable
use aes = Aes.Create()
aes.KeySize <- 256
aes.GenerateIV()
aes.GenerateKey()
// do! Task
do! File.WriteAllTextAsync("key.iv.txt", $"Key: {Convert.ToBase64String aes.Key}\nIV: {Convert.ToBase64String aes.IV}")
// use IAsyncDisposable
use outputStream = File.Create "secret.enc"
// use IDisposable
use encryptor = aes.CreateEncryptor()
// use IAsyncDisposable
use cryptoStream = new CryptoStream(outputStream, encryptor, CryptoStreamMode.Write)
// do! Task
do! exampleDomain.Content.CopyToAsync cryptoStream
}
Задачи значения
Задачи значения — это структуры, используемые для предотвращения выделения в программировании на основе задач. Задача значения — это эфемерное значение, которое превращается в реальную задачу с помощью .AsTask().
Чтобы создать задачу типа значения из выражения задачи, используйте |> ValueTask<ReturnType> или |> ValueTask. Рассмотрим пример.
let makeTask() =
task { return 1 }
makeTask() |> ValueTask<int>
and! привязки (начиная с F# 10)
В выражениях задач можно одновременно ожидать несколько асинхронных операций (Task<'T>и ValueTask<'T>Async<'T> т. д.). Сравните:
// We'll wait for x to resolve and then for y to resolve. Overall execution time is sum of two execution times.
let getResultsSequentially() =
task {
let! x = getX()
let! y = getY()
return x, y
}
// x and y will be awaited concurrently. Overall execution time is the time of the slowest operation.
let getResultsConcurrently() =
task {
let! x = getX()
and! y = getY()
return x, y
}
Добавление маркеров отмены и проверок отмены
В отличие от выражений Async F# выражения задач неявно передают маркер отмены и неявно выполняют проверки отмены. Если для кода требуется маркер отмены, необходимо указать маркер отмены в качестве параметра. Рассмотрим пример.
open System.Threading
let someTaskCode (cancellationToken: CancellationToken) =
task {
cancellationToken.ThrowIfCancellationRequested()
printfn $"continuing..."
}
Если вы планируете правильно отменить код, тщательно проверьте, передаете маркер отмены всем операциям библиотеки .NET, поддерживающим отмену. Например, Stream.ReadAsync имеет несколько перегрузок, одна из которых принимает токен отмены. Если вы не используете эту перегрузку, эта конкретная асинхронная операция чтения не будет отменена.
Ограничивающие параллелизм и конвейеры результатов
Чтобы ограничить одновременное количество операций, предоставляет Task.parallelLimitFSharp.Core 11. Передайте фабрики, принимаюющие маркеры отмены, а не задачи, которые уже начались:
open System.IO
open System.Threading
let readFiles (paths: seq<string>) (ct: CancellationToken) =
paths
|> Seq.map (fun path ->
fun token -> File.ReadAllTextAsync(path, token))
|> Task.parallelLimit 4 ct
Результирующий массив сохраняет входной порядок. Сбой запрашивает отмену одноуровневой работы, и каждая операция должна наблюдать за его маркером.
Task.sequential выполняет фабрики по одному за раз; Task.parallelDoLimit и Task.sequentialDo возвращается unit вместо массива.
Сведения Task.mapо Task.bindTask.catch конвейерах результатов и сопоставлении вспомогательных элементов Async/ValueTask см. в разделе асинхронные и целевые функции.
Фоновые задачи
По умолчанию задачи .NET планируются с использованием SynchronizationContext.Current, если он присутствует. Это позволяет задачам служить в качестве совместных, чередующихся агентов, выполняемых в потоке пользовательского интерфейса без блокировки пользовательского интерфейса. Если отсутствует, продолжение задач планируется в пуле потоков .NET.
На практике часто желательно, чтобы код библиотеки, создающий задачи, игнорирул контекст синхронизации и при необходимости всегда переключается на пул потоков .NET. Это можно сделать с помощью backgroundTask { }:
backgroundTask { expression }
Фоновая задача игнорирует любой SynchronizationContext.Current по следующему принципу: если она запустилась в потоке с ненулевым SynchronizationContext.Current, она переключается на фоновый поток в пуле потоков с использованием Task.Run. Если он запущен в потоке с значением NULL SynchronizationContext.Current, он выполняется в том же потоке.
Замечание
На практике это означает, что вызовы ConfigureAwait(false) обычно не требуются в коде задачи F#. Вместо этого задачи, которые должны выполняться в фоновом режиме, следует создавать с помощью backgroundTask { ... }. Любая привязка внешней задачи к фоновой задаче будет повторно синхронизироваться с SynchronizationContext.Current, когда фоновая задача будет завершена.
Ограничения задач в отношении хвостовых вызовов
В отличие от асинхронных выражений F#, выражения задач не поддерживают хвостовые вызовы. То есть, когда выполняется 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#, которое поддерживает хвостовые вызовы. Рассмотрим пример.
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#.
Асинхронные эксперименты среды выполнения
Авторы построителя могут экспериментировать с асинхронным асинхронным .NET среды выполнения. Созданный компилятором компьютер состояния обычно сохраняет состояние, необходимое для продолжения после ожидания. При асинхронной среде выполнения среда выполнения приостанавливает и возобновляет специально помеченный метод.
Замечание
Этот прототип использует функцию F# 11.2. В пакете SDK .NET 11.0.100 включите <LangVersion>preview</LangVersion> компилятор, содержащий поддержку среды выполнения-асинхронной среды выполнения. Для нее также требуются сборки среды выполнения и ссылочные сборки MethodImplOptions.AsyncSystem.Runtime.CompilerServices.AsyncHelpers.Await. Эталонные сборки описывают API во время компиляции; Выполняющаяся среда выполнения .NET должна реализовать поведение приостановки и возобновления работы.
Пример проверенного сочетания компонентов:
| Component | Версия или требование |
|---|---|
| Компилятор | Компилятор F# 11, предоставленный FSharp.Compiler.Service 43.13.101-rc2.26473.112. |
| FSharp.Core | Пакет 11.0.101-rc2.26473.112с помощью lib/net10.0/FSharp.Core.dll. Эта сборка поставляется StateMachineHelpers.__runtimeAsyncReturn; netstandard2.0 и netstandard2.1 сборки не создаются. |
| целевой объект Project |
net11.0. |
| Сборки среды выполнения и ссылки |
11.0.0-rc.1.26425.128.NET . |
Эти проверенные компоненты не являются одним дистрибутивом пакета SDK. .NET 10.0.12 предоставляет справочные API, но проверка приостановки и возобновления завершается ошибкой во время выполнения.TypeLoadException Используйте проверенную комбинацию среды выполнения и ссылки выше; ни режим предварительной версии, ни присутствие API не поддерживает среду выполнения.
Следующий сайт вызова использует построитель, определенный приложением, для асинхронного чтения файла:
let readManifest (path: string) =
runtimeTask {
let! text = System.IO.File.ReadAllTextAsync path
return text.Length
}
Построитель подключает выражение вычисления к асинхронной возвращаемой встроенной и ожидающей поддержке среды выполнения. Это не новый встроенный task построитель:
open System.Threading.Tasks
open System.Runtime.CompilerServices
open Microsoft.FSharp.Core.CompilerServices
type RuntimeTaskBuilder() =
member inline _.Delay([<InlineIfLambda>] code: unit -> 'T) = code
member inline _.Run([<InlineIfLambda>] code: unit -> 'T) : Task<'T> =
StateMachineHelpers.__runtimeAsyncReturn (code())
member inline _.Return(value: 'T) = value
member inline _.Bind(work: Task<'T>, [<InlineIfLambda>] next: 'T -> 'U) =
next (AsyncHelpers.Await work)
let runtimeTask = RuntimeTaskBuilder()
__runtimeAsyncReturn помечает закрытый метод для асинхронной среды выполнения. Внутри этого метода AsyncHelpers.Await можно приостановить работу без завершения и вернуть элемент управления вызывающей стороне, а затем возобновить работу с результатом. Это не блокирующий .Result вызов.
Компилятор проверяет поддерживаемые шаблоны приостановки; byrefs не может оставаться в живых через приостановку. В примере определяются Delayтолько , RunReturnи одна Bind перегрузка, а не рабочий построитель с отменой, циклами или асинхронным удалением.
Включите функцию F# 11.2, в которой встроенный построитель определен и развернут. Вызывающие интерфейсы уже скомпилированного readManifest API не нуждаются в нем. Обычные task выражения сохраняют свою реализацию. Используйте AsyncHelpers.Await только в асинхронных методах среды выполнения. См. асинхронную спецификацию среды выполнения.
Полный пример проекта включает точку входа консоли. В каталоге запустите dotnet run -- manifest.txt совместимые средства и файл для чтения. Файл, содержащий точно manifest, без последней новой линии, создает 8. Блоки точки входа консоли для завершения; readManifest возвращается Task<int>.