Асинхронность в F# — async, task и агенты
Асинхронность в F# - async, task и агенты
Запрос в интернет, чтение файла, ожидание ответа БД — долгие операции. Пока поток ждёт сеть, его можно отдать другим задачам. В .NET для этого используют Task и async/await в C#.
F# добавляет:
- вычислительное выражение
async { }— историческая модель F# с типомAsync<'T>; - вычислительное выражение
task { }(с F# 6) — обёртка надTask/Task<'T>, удобная рядом с ASP.NET Core и C#; - агенты (
MailboxProcessor) — очередь сообщений и одно «потоковое» обновление состояния.
В обзоре F# может встретиться классический async; здесь — как выбрать модель и не блокировать сервер.
Синхронный код и блокировка (на пальцах)
Синхронный вызов:
let text = client.GetStringAsync(url).Result // плохая практика на сервере
.Result блокирует текущий поток, пока сеть не ответит. В ASP.NET Core это съедает потоки пула и снижает пропускную способность. Правильный путь — ожидать операцию внутри async или task без блокировки.
Две модели — async и task
async { } | task { } | |
|---|---|---|
| Тип результата | Async<'T> | Task<'T> |
| Запуск в F# | Async.RunSynchronously, Async.Start | task.Result (осторожно), await из C# |
| Вызов из C# | через Async.StartAsTask | await напрямую |
| Новый веб на .NET 6+ | допустимо | предпочтительно для публичного API |
let! внутри блока — «дождаться асинхронной операции и продолжить с результатом» (аналог await в C#).
return — итоговое значение блока.
use / use! — освободить IDisposable при выходе из блока (закрыть поток, клиент).
Блок async — разбор примера
open System.Net.Http
let fetchLength (url: string) =
async {
use client = new HttpClient()
let! text = client.GetStringAsync(url) |> Async.AwaitTask
return text.Length
}
let len = fetchLength "https://example.com" |> Async.RunSynchronously
Построчно:
use client = new HttpClient()— клиент будет уничтожен в конце блока.GetStringAsyncв .NET возвращаетTask<string>, не строку сразу.|> Async.AwaitTask— мост Task → Async, чтобы использоватьlet!вasync.return text.Length— длина строки как результатAsync<int>.Async.RunSynchronously— дождаться в текущем потоке (допустимо в консоли и тестах; на сервере в обработчике запроса — нет).
Параллельно несколько URL:
let urls = [ "https://example.com"; "https://dotnet.microsoft.com" ]
let lengths =
urls
|> List.map fetchLength
|> Async.Parallel
|> Async.RunSynchronously
Async.Parallel запускает несколько Async и собирает массив результатов. Для чистого CPU на всех ядрах смотрят также Task и пулы потоков.
Блок task — разбор примера
let fetchLengthTask (url: string) =
task {
use client = new HttpClient()
let! text = client.GetStringAsync url
return text.Length
}
GetStringAsync здесь сразу ожидается в task — без Async.AwaitTask. Возвращаемый тип — Task<int>. Такой метод естественно вызывать из C#:
int len = await fetchLengthTask("https://example.com");
Правило для новых библиотек на .NET 6+: внешний контракт — Task / ValueTask; внутри F#-модуля — task и чистые функции.
Переход Task ↔ Async
// Task -> Async
let taskToAsync (t: System.Threading.Tasks.Task<'T>) = Async.AwaitTask t
// Async -> Task
let asyncToTask a = Async.StartAsTask a
Библиотека для C# должна экспортировать Task, а не заставлять потребителей знать про Async.
Ресурсы и исключения
let readFile path =
async {
use stream = new System.IO.FileStream(path, System.IO.FileMode.Open)
use reader = new System.IO.StreamReader(stream)
return reader.ReadToEnd()
}
Исключения внутри async/task ловят try/with как в обычном коде. Для ожидаемых сбоев (файл не найден, неверный id) предпочтительнее Result и match (186), а исключения — для действительно аварийных ситуаций.
MailboxProcessor — агент с очередью
Агент — объект с почтовым ящиком: сообщения приходят в очередь, обрабатываются по одному в цикле. Состояние меняется только внутри этого цикла — проще рассуждать, чем о lock на общем mutable.
type CounterMsg =
| Add of int
| Get of AsyncReplyChannel<int>
let createCounter () =
MailboxProcessor.Start(fun inbox ->
let rec loop total =
async {
let! msg = inbox.Receive()
match msg with
| Add n -> return! loop (total + n)
| Get reply ->
reply.Reply total
return! loop total
}
loop 0)
let agent = createCounter ()
agent.Post (Add 10)
let current = agent.PostAndReply (fun reply -> Get reply)
| Элемент | Смысл |
|---|---|
CounterMsg | DU — тип сообщений |
Add n | «увеличить на n» |
Get reply | «верни текущее значение через канал ответа» |
inbox.Receive() | асинхронно ждать следующее сообщение |
return! loop (...) | хвостовая рекурсия: новый цикл с обновлённым total |
Post | отправить сообщение без ответа |
PostAndReply | отправить и дождаться ответа |
| Подходит | Менее подходит |
|---|---|
| Очередь команд к одному ресурсу | Массовый параллельный CPU на всех ядрах |
| Троттлинг, батчинг событий | Один разовый task без состояния |
| Модель «актора» в процессе | Распределённый кластер без доп. библиотек |
Для высокой нагрузки смотрят System.Threading.Channels и Task; агент остаётся идиоматичным инструментом F#.
Типичные ошибки
Async.RunSynchronouslyили.Resultв обработчике HTTP — блокировка потока пула.- Общий
mutableиз нескольких потоков без агента илиlock— гонки данных. - Игнорирование отмены — передавать
CancellationTokenвGetStringAsyncи проверять в длинных циклах. - Новый
HttpClientна каждый запрос в production — один долгоживущий клиент илиIHttpClientFactoryв ASP.NET Core.
Отмена (кратко)
let fetch ct (url: string) =
task {
use client = new HttpClient()
let! text = client.GetStringAsync(url, ct)
return text.Length
}
CancellationToken прерывает ожидание, если операция ещё не завершилась — важно для таймаутов и остановки службы.
Дальше
- Структура F#-проекта
- Справочник по F#
- Справочник языка F# (Learn) — разделы async / task
- Императивные конструкции —
useв синхронном коде
См. также
Другие статьи этого же раздела в боковом меню (как на странице «О разделе»). Платформа .NET - архитектура экосистемы, инструменты разработки и модель выполнения приложений. Эти механизмы позволили реализовать фундаментальный принцип .NET — язык — это синтаксический фасад над общей семантикой CLR. Понимание архитектуры .NET невозможно без хронологического контекста, поскольку многие текущие решения — это результат многолетней итеративной оптимизации. Типы приложений на платформе .NET - веб, desktop, мобильные и облачные сценарии в единой экосистеме. Сборка и развёртывание .NET-приложений - артефакты, среды выполнения и практики доставки в продакшен. Пакеты и зависимости в .NET - управление версиями, восстановление пакетов и интеграция в процесс сборки. В Visual Studio проект — это единица сборки — он определяет, что и как компилируется. Проект содержит .csproj, исходные файлы, ресурсы и метаданные зависимостей. NuGet - система управления пакетами .NET для публикации, версионирования и подключения зависимостей. ADO.NET в .NET 8+ — Connection, Command, параметры и провайдеры; краткая история классического ADO (COM). ASP.NET - веб-платформа Microsoft для разработки серверных приложений, API и динамических сайтов. Экосистема .NET-приложений - поддерживаемые платформы, сценарии разработки и интеграция с современными устройствами. F# в экосистеме .NET - функциональный стиль, совместимость с платформой и применение в прикладной разработке.Платформа .NET
История платформы .NET
Архитектурные особенности .NET
Типы приложений на платформе .NET
Сборка и развёртывание .NET-приложений
Пакеты и зависимости в .NET
Инструменты разработки для .NET
NuGet - система управления пакетами
ADO.NET - доступ к данным
ASP.NET - веб-платформа Microsoft
Экосистема .NET-приложений
F# - функциональный язык в экосистеме .NET