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

F# выражает асинхронные операции декларативно через async-вычислительные выражения.

async в F# — вычислительное выражение, описывающее асинхронную операцию: оно строит «рецепт» работы, которая выполнится без блокировки потока.

Зачем асинхронность

Операции ввода-вывода (сеть, диск, запрос к БД) долгие. Блокировать поток ожиданием расточительно. Асинхронность позволяет «отпустить» поток на время ожидания и продолжить, когда результат готов — так сервер обслуживает тысячи запросов малым числом потоков.

Блок async { }

Асинхронный код оборачивают в async { ... }. Внутри let! ждёт результат другой асинхронной операции, не блокируя поток.

let fetchData () =
    async {
        // имитация асинхронной работы
        do! Async.Sleep 100
        return 42
    }

let result =
    fetchData ()
    |> Async.RunSynchronously
printfn "%d" result

Вывод:

42

do! ждёт асинхронную операцию без возврата значения; let! — с возвратом; return завершает асинхронный блок результатом. Async.RunSynchronously запускает «рецепт» и дожидается результата.

Композиция асинхронных шагов

Асинхронные операции естественно соединяются через let! — код читается почти как синхронный, но не блокирует поток.

let step x =
    async { return x * 2 }

let pipeline =
    async {
        let! a = step 10
        let! b = step a
        return a + b
    }

printfn "%d" (Async.RunSynchronously pipeline)

Вывод:

60

step 10 даёт 20, step 20 даёт 40, сумма — 60. Каждая let! «ждёт» результат, не занимая поток.

Параллельный запуск

Несколько независимых асинхронных операций можно запустить разом через Async.Parallel.

let tasks = [ step 1; step 2; step 3 ]
let results =
    tasks
    |> Async.Parallel
    |> Async.RunSynchronously
printfn "%A" results

Вывод:

[|2; 4; 6|]

Как работает под капотом

async { } — это вычислительное выражение: компилятор переписывает его в цепочку продолжений (continuations). let! регистрирует «что делать, когда результат готов», а поток тем временем свободен для другой работы. Async в F# совместим с Task из .NET (есть Async.AwaitTask и обратно), поэтому F# легко работает с асинхронными API библиотек C#. В отличие от Task, Async-значение «холодное» — оно не начинает выполняться, пока его явно не запустят.

Частые ошибки

  • Забыть Async.RunSynchronously (или Async.Start) — Async сам по себе ничего не делает.
  • Путать let! (ждёт async с результатом) и do! (ждёт без результата).
  • Блокировать поток синхронным ожиданием внутри async вместо let!.

Итоги

  • Асинхронный код пишут в блоке async { ... }.
  • let! и do! ожидают асинхронные операции без блокировки потока.
  • Async.RunSynchronously запускает «холодный» async и ждёт результат.
  • Async.Parallel выполняет независимые операции одновременно; есть мост к .NET Task.
Проверьте себя
1. Во что оборачивают асинхронный код в F#?
Atask { }
Basync { }
Cawait { }
Dfuture { }
2. Что делает let! внутри async-блока?
AБлокирует поток навсегда
BОжидает результат асинхронной операции, не блокируя поток
CСоздаёт новый поток на каждый вызов
DЗавершает программу
3. Почему Async-значение «холодное»?
AОно занимает мало памяти
BОно не выполняется, пока его явно не запустят
CОно работает только ночью
DОно всегда синхронно