Асинхронность

Введение в асинхронность

Последнее обновление: 15.12.2023

Нередко программа выполняет такие операции, которые могут занять продолжительное время, например, обращение к сетевым ресурсам, чтение-запись файлов, обращение к базе данных и т.д. Такие операции могут серьезно нагрузить приложение. Особенно это актуально в графических (десктопных или мобильных) приложениях, где продолжительные операции могут блокировать интерфейс пользователя и негативно повлиять на желание пользователя работать с программой, или в веб-приложениях, которые должны быть готовы обслуживать тысячи запросов в секунду. В синхронном приложении при выполнении продолжительных операций в основном потоке этот поток просто бы блокировался на время выполнения операции. И чтобы продолжительные операции не блокировали общую работу приложения, в F# можно задействовать асинхронность.

Асинхронные вычисления не зависят от основного потока программы и планируются независимо от основного потока программы. Это независимое выполнение не подразумевает параллелизм, что асинхронные вычисления и основновной поток выполняются параллельно или одновременно. Это также не означает, что асинхронные вычисления происходят в фоновом режиме. Фактически, асинхронные вычисления могут даже выполняться синхронно, в зависимости от характера вычислений и среды, в которой они выполняются.

Для определения асинхронной операции F# предоставляет два типа выражений:

  • async { выражения }: собственно представляет асинхронную операцию, позволяет определять композиции асинхронных операций

  • task { выражения }: позволяет определить задачу типа Task и применяется для совместимости с библиотеками и проектами .NET

При написании большей части асинхронного кода на F# Microsoft рекомендует использовать выражения async { ... }, поскольку они более краткие, позволяют создавать композиции операций и некоторых недостатков задач .NET. Но если необходимо взаимодействие с другим кодом .NET, например, с кодом на C#, то для совместимости применяется выражение task { выражения }.

Выражение async {}

Выражение 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

Async.Sleep

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 и т.д.

Привязка let!

Для упрощения работы с асинхронным кодом язык 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
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850