Открытый консольный текстовый редактор Neovim, форк от легендарного Vim, взял от своего родителя самое лучшее, внедрив при этом собственные новшества. В отличие от специфичного VimScript, редактор использует более быстрый и распространенный язык Lua. Преимуществ у этого решения много, но сегодня мы поговорим об одном из главных — асинхронности.
Зачем она вообще нужна в редакторе? Все просто: Neovim, как и большинство текстовых редакторов, работает в одном главном потоке. Если пытаться, например, выполнять сложный поиск по проекту с помощью синхронного кода, весь интерфейс редактора зависнет до окончания операции. Чтобы этого избежать, Neovim использует event loop на базе библиотеки Libuv (на которой, кстати, построен Node.js).
До появления модуля vim.async разработчикам приходилось использовать подходы, которые могли быть довольно неэффективными, а иногда и вовсе проблематичными. Например, при использовании vim.loop и vim.uv можно было попросить Neovim асинхронно прочитать файл, а после завершения передать результат callback-функции.
vim.uv.fs_open("input.txt", "r", 438, function(open_error, fd)
if open_error then
return print(open_error)
end
vim.uv.fs_read(fd, 4096, 0, function(read_error, data)
if read_error then
print(read_error)
else
print(data)
end
vim.uv.fs_close(fd)
end)
end)
Если продолжать усложнять цепочку таких вызовов, можно попасть в так называемый callback hell, нечитаемую «пирамиду смерти». В этом случае код можно было сделать более читаемым с помощью отдельных функций или оберток.
Корутины тоже помогали решать эту задачу: они позволяли приостанавливать выполнение функции до определенного момента. Благодаря этому асинхронный код можно было писать более линейно, но для этой цели использовались сторонние решения, включая plenary.async, async.nvim и собственные абстракции. Такой подход уменьшал вложенность, однако приводил к фрагментации, так как разные плагины могли использовать разные соглашения: о создании задач, обработке ошибок и так далее. Отсутствие единой модели осложняло взаимодействие и делало эффективное управление жизненным циклом задач более трудоемким.
Что изменилось с vim.async
Модуль vim.async добавляет в стандартную библиотеку встроенную модель структурированной конкурентности, выступая заменой собственных корутин-оберток. В рамках новой модели асинхронные подпрограммы выполняются внутри задач, создаваемых с помощью vim.async.run(). Когда задача ожидает события или операции ввода-вывода c использованием vim.async.await(), NeoVim останавливает выполнение фрейма и возвращает управление event loop. Это гарантирует, что синхронные операции редактора и пользовательский ввод продолжатся без прерываний.
Появляется строгая иерархия этих задач, в которой обеспечиваются четкие отношения между родительскими и дочерними элементами. В этой иерархии родительская задача не разрешится, пока не будут завершены все дочерние. Более того, необработанные исключения внутри дочерней задачи немедленно передаются родительской, запуская отмену во всех родственных задачах, если они не были изолированы. Есть и исключения: если разработчику необходимо, чтобы асинхронный процесс все равно продолжал работу после завершения инициирующей задачи, то вызов task:detach() переведет его работу в статус независимого процесса высокого уровня.
На данный момент представлены следующие примитивы, с помощью которых можно управлять асинхронностью:
vim.async.semaphore()— ограничивает количество одновременных операций. Это может быть полезным, например, при обработке большого количества файлов или сетевых запросов.vim.async.timeout()— задает временное ограничение для операции или групп операций, отменяя их выполнение при превышении заданного времени.vim.async.iter()— позволяет получать результаты выполняющихся задач в порядке их фактического завершения, а не в порядке запуска.vim.async.pawait()— является асинхронным аналогомpcall(). Вместо распространения ожидаемой ошибки вызывающий код получит флаг состояния, просто с сообщением об этой самой ошибке.
Выводы
Реакция сообщества на эти изменения была в основном положительной. Единая асинхронная абстракция стала «пластырем» для такой болевой точки экосистемы как несовместимость плагинов, вызванная конкурирующими сторонними библиотеками. Конечно, дискуссии по решению тоже есть, например вызывают вопросы нюансы распространения ошибок и, в частности, само взаимодействие vim.async.await() с Libuv, однако изменение явно решает проблемы многих разработчиков, а это самое главное.