Примечание
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
В этой статье описывается поддержка в 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 .. else
if .. 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
имеет несколько перегрузок, одна из которых принимает токен отмены. Если вы не используете эту перегрузку, эта конкретная асинхронная операция чтения не будет отменена.
Фоновые задачи
По умолчанию задачи .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#.