Нередко программа выполняет такие операции, которые могут занять продолжительное время, например, обращение к сетевым ресурсам, чтение-запись файлов, обращение к базе данных и т.д. Такие операции могут серьезно нагрузить приложение. Особенно это актуально в графических (десктопных или мобильных) приложениях, где продолжительные операции могут блокировать интерфейс пользователя и негативно повлиять на желание пользователя работать с программой, или в веб-приложениях, которые должны быть готовы обслуживать тысячи запросов в секунду. В синхронном приложении при выполнении продолжительных операций в основном потоке этот поток просто бы блокировался на время выполнения операции. И чтобы продолжительные операции не блокировали общую работу приложения, в F# можно задействовать асинхронность.
Асинхронные вычисления не зависят от основного потока программы и планируются независимо от основного потока программы. Это независимое выполнение не подразумевает параллелизм, что асинхронные вычисления и основновной поток выполняются параллельно или одновременно. Это также не означает, что асинхронные вычисления происходят в фоновом режиме. Фактически, асинхронные вычисления могут даже выполняться синхронно, в зависимости от характера вычислений и среды, в которой они выполняются.
Для определения асинхронной операции F# предоставляет два типа выражений:
async { выражения }
: собственно представляет асинхронную операцию, позволяет определять композиции асинхронных операций
task { выражения }
: позволяет определить задачу типа Task и применяется для совместимости с библиотеками и проектами .NET
При написании большей части асинхронного кода на F# Microsoft рекомендует использовать выражения async { ... }
, поскольку они более краткие,
позволяют создавать композиции операций и некоторых недостатков задач .NET. Но если необходимо взаимодействие с другим кодом .NET, например, с кодом на C#, то для совместимости
применяется выражение task { выражения }
.
Выражение async { .. } создает объект типа Async<'T>, где 'T — это тип, возвращаемый выражением с помощью оператора return. Если блок async ничего не возвращает, то результат блока имеет тип Async<'unit>
Выражения, которые помещаются внутри фигурных скобок, конфигурируются для асинхронного выполнения - без блокировки текущего потока при выполнении асинхронных операций. Асинхронные выражения часто запускаются в фоновом потоке, а выполнение продолжается в текущем потоке.
Для начала определим следующую программу на F#:
async { printfn "Async works" printfn "Async ends" } printfn "Program works" printfn "Program ends"
С помощью функции printfn на консоль выводятся четыре строки, две из них в блоке async. Консольный вывод данной программы будет следующим:
Program works Program ends
Мы видим, что выражения в блоке async не выполняются. Для выполнения асинхронных вычислений нам надо их запустить.
Чтобы запустить выражения в блоке async, можно применять ряд методов типа Async:
Async.RunSynchronously: запускает асинхронные вычисления и ожидает результат.
Async.Start: запускает асинхронные вычисления и не ждет результат.
Async.StartAsTask: запускает асинхронные вычисления как задачу типа Task.
Async.StartImmediate: сразу запускает асинхронные вычисления на текущем потоке операционной системы.
Все эти функции в качестве первого параметра принимают объект Async, в качестве которого может выступать результат блока async{...}
. Например, применим для запуска асинхронных вычислений функцию Async.Start
async { printfn "Async works" printfn "Async ends" } |> Async.Start // запускаем асинхронные вычисления printfn "Program works" printfn "Program ends"
С помощью оператора |> передаем результат блока async - объект Async<unit> в функцию Async.Start. При выполнении мы можем увидеть, что выполнение выражений блока async может происходит после остальных инструкций программы. Например, можно получить такой консольный вывод:
Program works Program ends Async works Async ends
F# предоставляет ряд функций, которые сами по себе уже представляют асинхронные вычисления и могут выполняться асинхронно. Одна из наиболее используемых - это функция Async.Sleep, которая выполняем задержку на определенное количество миллисекунд. Например:
printfn "Program works" Async.Sleep(2000) |> Async.RunSynchronously printfn "Program ends"
Здесь между двумя вызовами функции printfn
выполняется задержка в 2000 миллисекунд. И поскольку Async.Sleep
также представляет асинхронные вычисления,
то для их запуска применяется функция Async.RunSynchronously. Данная функция будет ожидать выполнения Async.Sleep
, и только после этого
начнет выполняться второй вызов printfn
.
Или чуть более показательный пример:
async { printfn "Async works" printfn "Async ends" } |> Async.Start // запускаем асинхронные вычисления printfn "Program works" Async.Sleep(2000) |> Async.RunSynchronously // выполняем задержку в 2 секунды printfn "Program ends"
Здесь запускаются двое асинхронных вычислений. Но блок async будет выполняться через некоторое время после запуска. Тогда как функция Async.Sleep будет выполняться тут же. В итоге мы можем получить следующий консольный вывод:
Program works Async works Async ends Program ends
То есть последняя инструкция printfn "Program ends"
будет выполняться последней после блока async, поскольку блок async скорее всего выполниться
за время 2-секундной задержки.
Асинхронные вычисления блока async можно определять в виде функции или значения:
let printMessages() = async { printfn "Async works" printfn "Async ends" } printMessages() |> Async.Start // запускаем асинхронные вычисления printfn "Program works" Async.Sleep(2000) |> Async.RunSynchronously // выполняем задержку в 2 секунды printfn "Program ends"
Однако такие функции сами по себе не запускаются, для их запуска опять же надо использовать функции Async.Start / Async.RunSynchronously
и т.д.
Для упрощения работы с асинхронным кодом язык F# позволяет выполнять ряд асинхронных привязок. Наиболее используемая - привязка let! позволяет получить результат асинхронных вычислений. Эффект let! заключается в том, чтобы разрешить продолжение выполнения других вычислений или потоков во время выполнения вычисления, к которому идет привязка. Например:
let getNumber n = async { Async.Sleep(500) |> Async.RunSynchronously return n * n } let printNumber n = async { printfn "Получение квадрата числа %d" n let! result = getNumber n printfn "Квадрат числа %d равен %d" n result } printNumber 1 |> Async.RunSynchronously printNumber 2 |> Async.RunSynchronously printNumber 3 |> Async.RunSynchronously
Асинхронный блок printNumber выполняет привязку let! к результату другой асинхронной функции - getNumber
let! result = getNumber n
Строка кода с let! запускает асинхронную функцию, а текущий поток приостанавливается до тех пор, пока не будет получен результат, после чего выполнение продолжается.
В асинхронной функции getNumber после небольшой задержки возвращается квадрат числа переданного через параметр.
В основной части программы запускаем функцию printNumber, передавая ей число. Чтобы программа не схлопнулась до выполнения асинхронных функций, оин запускаются синхронно с помощью
Async.RunSynchronously
printNumber 1 |> Async.RunSynchronously
Консольный вывод программы:
Получение квадрата числа 1 Квадрат числа 1 равен 1 Получение квадрата числа 2 Квадрат числа 2 равен 4 Получение квадрата числа 3 Квадрат числа 3 равен 9