Меня зовут Василий Ханевич, я хочу рассказать про использование асинхронных методов в платформе 1С:Предприятие 8.
Что с чем выполняется синхронно при использовании синхронного кода?
Для начала нужно навести порядок с терминологией. Что такое «синхронный» и «асинхронный» код?
Если мы будем выяснять смысл слова «синхронный», то можем найти, что это значит «одновременный». Однако про какую одновременность может идти речь, если мы знаем, что код на встроенном языке 1С исполняется последовательно?
Здесь можно привести аналогию с лентой конвейера. Интерпретатор берет команду, выполняет ее, переходит к следующей команде, выполняет следующую команду и так далее.
Важно, что выполняющийся метод блокирует основной поток выполнения кода до тех пор, пока этот метод не отработает. И если выполняющийся код будет вызывать другие процедуры или функции, необходимо будет дождаться их завершения.
Попробуем рассмотреть это на примере.
На экране вы видите простейший код – здесь вызывается функция для формирования текста сообщения. И это сообщение выводится на экран пользователю.
Эти команды выполняются в следующем порядке:
-
Сначала мы присваиваем значение переменной и вызываем функцию.
-
Далее выполняем программный код внутри вызванной функции – причем в тот момент, когда выполняется вызванная функция, основной код не выполняется.
-
Когда мы закончили функцию и вышли из нее, выводим текст на экран пользователю.
Здесь все просто и понятно.
Такой код называется синхронным, потому что между вызывающим и вызываемым кодами есть синхронизация по времени. Вызывающий код не будет исполняться в то время, когда работает вызванный код.
Зачем понадобились асинхронные методы?
Вроде все логично, понятно – выполняем код по шагам. Так зачем тогда понадобились асинхронные методы?
Рассмотрим несколько примеров, которые нарушают эту стройную модель.
Первый пример – это длительные вычисления. Выход из длительного метода не будет выполнен до тех пор, пока весь этот метод полностью не отработает.
Это приводит к тому, что приложение не будет реагировать на пользовательские действия. Ну или, как говорят наши любимые пользователи: «Колесико крутится и ничего не происходит».
Следующий пример – это методы, которые требуют реакции пользователя.
Сюда относятся: «Вопрос», «Предупреждение». Мы задали вопрос и ждем, пока пользователь выберет какой-то вариант ответа. Пока он не ответит, у нас поток выполнения кода будет заблокирован.
Но ведь методы типа «Вопрос» и «Предупреждение» были еще с 7.7, и все работало. Почему это вдруг стало проблемой?
Действительно, тонкий клиент будет работать, все будет отлично. Проблема возникает в веб-клиенте.
Дело в том, что мы используем браузер для работы с электронной почтой, банковскими приложениями, CRM-системами. Логично, что и учетная система будет работать в браузере. И фирма «1С» активно развивает это направление, развивает веб-клиент, чтобы он использовался.
Браузеры используются вообще везде. И они диктуют свои требования, накладывают ряд ограничений. И если учетная система должна работать в браузере, фирма «1С» должна выполнять все эти требования и вносить изменения в платформу. Поддерживать все требования, которые диктуются ей сверху браузерами.
Фактически, веб-клиент – это обыкновенное JavaScript-приложение.
А движок JavaScript в браузере – это всем нам хорошо известный Event Loop (событийный цикл).
-
JavaScript-приложение ждет поступающей команды.
-
Находит задачу, которую нужно выполнить, выполняет ее – это может быть скрипт, реакция на нажатие мыши, на нажатие кнопок на клавиатуре.
-
Закончили задачу, переходим в ожидание, ждем следующую задачу.
-
Появилась задача – отработали, выполнили ее. И так в цикле.
Движок JavaScript в браузере однопоточный. Это означает, что мы можем или выполнять код, или реагировать на действия пользователя. В связи с этим накладывается ряд требований:
-
Браузеры требуют, чтобы окно браузера было отзывчивое, и устанавливают ограничение по длительности выполнения скрипта. Если скрипт не укладывается в этот промежуток, мы видим в браузере сообщение как на экране – это скриншот из Firefox. В других браузерах сообщение похожее, но внешний вид может отличаться.
-
Кроме этого, браузер требует, чтобы работа с файлами на клиенте выполнялась только асинхронно.
Соблюдение этих требований важно, чтобы основной поток выполнения кода не блокировался.
Следовательно, обычный синхронный код, где мы должны дождаться, чтобы все отработало, здесь не подойдет. Здесь нужна уже другая парадигма, другой способ выполнения. Поэтому и появились асинхронные методы.
Асинхронные методы – это методы, у которых нет синхронизации по времени между вызывающим и вызываемым кодом.
Мы вызвали какой-то метод и не дожидаемся, пока он отработает, а сразу переходим к выполнению следующей команды.
За счет этого мы получаем сверхважное преимущество – у нас не блокируется основной поток выполнения кода.
При помощи такой асинхронной модели мы можем выполнять следующие действия:
-
Работа с блокирующими окнами: задать вопрос, показать предупреждение.
-
Работа с файлами и каталогами.
-
Работа с механизмами криптографии.
-
Работа с внешними компонентами.
Асинхронность, про которую мы говорим, доступна только на клиенте. Потому что она ориентирована на веб-клиент, на ограничения JavaScript-приложения и требования браузеров.
А на сервере выполняется синхронный код, так как сервер 1С - это собственное приложение, а не браузер. Там делаем, что хотим. Поэтому на сервере можно свободно использовать КопироватьФайл. А вот на клиенте мы должны использовать асинхронные аналоги.
Здесь приведено два варианта асинхронных методов:
-
Процедура НачатьКопированиеФайла;
-
Новая асинхронная функция КопироватьФайлАсинх – у всех новых асинхронных функций есть постфикс Асинх.
Для работы с асинхронными методами есть два подхода:
-
Callback’и – методы обратного вызова;
-
Обещания – функциональность платформы 1С:Предприятие, появившаяся в платформе 8.3.18.
Рассмотрим их по порядку.
Что такое методы обратного вызова (callback’и)?
Принцип действия Callback’ов следующий:
Мы вызываем какой-то асинхронный метод и не дожидаемся, когда он отработает и полностью выполнит все необходимые действия – мы сразу переходим к следующей команде.
Но в тот момент, когда этот асинхронный код отработает, он оповестит нас, скажет: «Все, я закончил». И даст доступ к результату.
Мы сможем узнать, какой вариант ответа выбрал пользователь, или как он отреагировал на какое-то действие.
Как это может быть реализовано?
В других языках, например, в JavaScript, можно передать функцию в качестве параметра другого метода.
Например, мы можем вызвать некую асинхронную функцию, чтобы она выполнила необходимые действия. И кроме параметров мы в эту функцию передаем Callback – говорим ей, куда нужно вернуться.
Callback в переводе с английского означает “обратный звонок”. Куда нужно обратно позвонить после того, как мы выполним все действия.
Когда все действия выполнены, происходит обратный звонок – мы вызываем какую-то процедуру, где уже обрабатываем результат асинхронного метода.
Но в 1С мы не можем использовать функцию как параметр другого метода. Поэтому в платформе 1С у нас появляется такой объект, как ОписаниеОповещения.
ОписаниеОповещения – это оболочка, в которой мы говорим:
-
Какую процедуру нужно выполнить;
-
В каком модуле она будет располагаться;
-
Какие параметры в нее нужно передать.
При разработке удобно использовать Синтакс-помощник. В нем для асинхронного метода будут указаны:
-
Описание оповещения;
-
Параметры, которые должны быть у метода, используемого в качестве callback’а.
Например, у процедуры, используемой в качестве ОписанияОповещения метода ПоказатьВопрос, должен быть параметр РезультатВопроса.
Какие сложности могут возникнуть при использовании методов обратного вызова?
На слайде показано, как изменится программный код, если мы используем асинхронную модель.
Что бросается в глаза? Линейный по смыслу код распадается на два фрагмента:
-
Инициация вопроса пользователю;
-
Обработка результата.
Также мы видим, что процедура, используемая в качестве метода обратного вызова, у нас экспортная – это такое ограничение.
На схеме показано, как это работает:
-
Мы инициируем показ вопроса и не ждем, пока пользователь ответит – мы сразу переходим к следующей строке программного кода.
-
И только тогда, когда пользователь ответит на заданный вопрос, будет вызван метод обратного вызова, в котором мы обработаем результат.
Синяя пунктирная стрелка показывает логическую связь основного метода и метода обратного вызова.
Рассмотрим более сложный пример – нам нужно сформировать файл по данным информационной базы и сохранить его на компьютере пользователя:
-
Сначала формируем файл на сервере;
-
Потом спрашиваем у пользователя, куда его нужно сохранить;
-
Сохраняем и выдаем уведомление, что все готово, метод отработал, файл сохранен там-то.
Как это реализовать в программе по шагам:
-
Передаем управление на сервер – на нем в каталоге временных файлов формируется файл с нужными данными. Чтобы была возможность доступа к этим данным из клиентских процедур, помещаем его во временное хранилище.
-
Затем с помощью асинхронного метода Показать на клиенте показываем пользователю диалоговое окно с выбором пути сохранения файла. Раз это асинхронный метод, значит, нужен callback – процедура, куда и будет передан адрес во временном хранилище, где располагаются данные требуемого файла.
-
Когда пользователь укажет путь сохранения файла, callback сработает, и мы вызовем второй асинхронный метод НачатьПолучениеФайлаССервера. В этом методе инициируется получение файла с сервера и его сохранение на клиентском компьютере.
-
Когда второй асинхронный метод отработал, нужен еще один callback, где уже можно вывести уведомление о том, что загрузка файла завершена.
Казалось бы, по логике это должен быть линейный код. Мы сформировали файл на сервере, спросили, куда сохранить, загрузили файл с сервера и вывели уведомление.
Но в программном коде мы получаем фрагментированный код – линейный код распадается на отдельные фрагменты.
В других языках, в которых можно функцию передать в качестве параметра другого метода, есть такой термин, как CALLBACK HELL – ад callback’ов. На экране показано, как он выглядит.
Но в 1С мы не можем указывать функцию в качестве параметра другого метода. И, чтобы показать, как CALLBACK HELL выглядит применительно к 1С, нужно подобрать другой пример.
В качестве такого примера, реализованного в разных конфигурациях, можно рассмотреть закрытие кассовой смены, когда после завершения кассовой смены пользователю нужно сформировать документы выемки, снять Z-отчет и т.д.
На экране выведены действия, которые должны быть выполнены. Причем это я так поджал еще, чтобы на слайд влезло. На самом деле там ещё много действий, где нужно что-то спросить у пользователя, вывести какое-то уведомление, взаимодействовать с кассовой техникой.
И если для простого вопроса у нас код распадается на фрагменты, то здесь вообще ужас получается – большое количество фрагментированного кода.
А если у нас разные процедуры, для них всех нужно имена придумывать. Вот что происходит в процедуре ПродолжитьЗакрытиеСменыОкончание или ПродолжитьЗакрытиеСменыОкончаниеФискальныйОтчетЗавершение? Непонятно.
Код сложный, фрагментированный. Читать сложно, дорабатывать еще сложнее.
Что такое Обещание (promise), которое появилось в платформе 8.3.18?
Возникает вопрос: что можно сделать, чтобы реализация асинхронности стала проще для разработки?
Один из таких вариантов был сделан в платформе 8.3.18 – там появились Обещания. И асинхронность уже можно реализовать с использованием Обещаний.
Основная идея такой реализации – это приблизить асинхронный код к обычному последовательному коду.
В качестве аналога можно взять другой язык программирования, тот же JavaScript. Там тоже есть Promise, async/await. Аналогичная реализация этого подхода и была сделана в платформе 8.3.18.
Важная оговорка – эта функциональность не завязана на режим совместимости конфигурации. Например, мы можем взять типовую Бухгалтерию предприятия, где установлен режим совместимости 8.3.14. При использовании платформы 8.3.18 вместо callback’ов можно применять асинхронные методы с использованием Обещаний.
Давайте разберемся, как устроены Обещания.
Фактически Обещание – это хранилище для результата асинхронной функции, который мы еще пока не знаем.
Когда мы вызвали какую-то асинхронную функцию, ей потребуется какое-то время, чтобы отработать. Но в случае обещания мы не ждем завершения работы этой функции и продолжаем выполнять следующие команды программного кода.
-
Мы еще не знаем результат работы этой асинхронной функции – в этот момент Обещание находится в статусе «Ожидание». Оно выделено на экране желтым цветом.
-
Потом прошло время, асинхронная функция отработала, и все успешно завершилось. Статус «Завершено успешно» выделен на слайде зеленым.
-
Второй вариант – в процессе выполнения асинхронной функции возникло исключение. Вариант «Завершено с ошибкой» выделен на слайде красным.
Эта схема – жизненный цикл Обещания от статуса Ожидание до успешного завершения или возникновения исключения.
Фактически объект Обещание – это альтернатива объекту ОписанияОповещения.
Результат асинхронной функции будет содержаться в самом объекте типа Обещание.
Еще раз обращаю внимание, что объект типа Обещание доступен только на клиенте. Та асинхронность, про которую мы сейчас говорим – исключительно клиентская. Она пришла из-за требований браузера, поэтому Обещание у нас только клиентское.
Следующий важный момент – это то, что Обещание не имеет конструкторов, методов, свойств. Оно может быть получено исключительно в результате работы асинхронной функции. Других вариантов нет.
На слайде показана табличка с соответствием методов.
-
Левая колонка – это синхронный метод, например, Вопрос.
-
В центре – асинхронная процедура с callback’ом. Именно процедура. Например, ПоказатьВопрос – это процедура.
-
Третья колонка – это новые асинхронные функции, которые будут возвращать Обещание. Именно они появились в платформе 8.3.18. Например, ВопросАсинх. Если вы посмотрите на название этих функций, то увидите, что у них всех постфикс «Асинх» – они заканчиваются на «Асинх».
Чтобы не держать в голове это соответствие, можно снова воспользоваться Синтакс-помощником.
Например, мы хотим вспомнить асинхронные варианты метода Вопрос. Находим его, и там в «См. также» будет список соответствий – или ВопросАсинх, или ПоказатьВопрос. Не запутаетесь.
Как использовать оператор Ждать?
Итак, мы разобрались, что такое Обещание. Выяснили, что асинхронные функции всегда возвращают значение типа Обещание.
Но нам же нужно из этого Обещания получить какой-то результат!
Чтобы расшифровать это Обещание и получить из него результат, во встроенном языке был реализован оператор Ждать. Он тоже появился в платформе 8.3.18.
Синтаксис получения результата из асинхронной функции:
Результат = Ждать Обещание;
А теперь вопрос. Что делает программный код:
Ждать 1000;
Неужели мы дождались, и наконец 1С добавила во встроенный язык паузу?
Конечно, нет, не дождались.
Если справа от оператора Ждать находится не Обещание, а, например, число 1000, то это – значение, которое будет завернуто в Обещание.
Программный код «Ждать 1000» – это корректный программный код, но фактически это значит, что в обещании завернуто константное значение 1000. Это не способ организации паузы.
Как использовать модификатор Асинх для описания собственных асинхронных процедур и функций?
Итого, у нас есть Обещание и оператор Ждать.
Но этого оказалось мало. Если разработчику нужно реализовать процедуру или функцию, которая будет работать асинхронно, ее нужно описать с использованием модификатора Асинх.
Выглядит это вот так:
-
Перед словом Функция мы указываем Асинх.
-
При этом оператор Ждать можно использовать только внутри процедур и функций, которые объявлены с модификатором Асинх.
-
Очередной раз оговорюсь – все это касается только клиентских методов. Поэтому модификатор Асинх можно использовать только для клиентских методов.
На экране показано, как будет выглядеть программный код:
-
Указан модификатор Асинх;
-
Внутри процедуры используется оператор Ждать;
-
Функция ВопросАсинх возвращает Обещание.
Какие ошибки могут встретиться при использовании асинхронных функций в коде?
Поскольку эта тема – новая, при работе могут возникать ошибки. Давайте рассмотрим самые распространенные.
Например, программный код, показанный на слайде, приведет к ошибке. Почему? Потому что модификатор Асинх мы используем с аннотацией &НаСервере. Так нельзя. Его можно использовать только в клиентском контексте.
Следующая ошибка возникает потому, что мы используем оператор Ждать, но этот метод не объявлен как асинхронный. Не хватает модификатора Асинх.
Если мы его добавим, все будет окей. Иначе возникнет ошибка.
Следующая ошибка – казалось бы, абсолютно корректный код, мы все учили: Ждать есть, Асинх есть.
Но обратите внимание, что Асинх и Ждать не выделены красным – платформа не распознает их как ключевые слова. Дело в том, что это – скриншот с платформы 8.3.17, где не было еще этой функциональности.
Если мы используем 8.3.18, этот код будет работать. С предыдущей платформой он не будет работать, будет выдавать ошибки.
Вся эта асинхронная магия происходит внутри оператора Ждать.
Логически нам кажется, что мы в этом операторе ждем, пока пользователь ответит на вопрос.
Но важно понимать, что технически происходит приостановка выполнения текущего метода и выход из процедуры – управление возвращается назад к коду, который его вызвал. При этом сохраняются значения локальных переменных.
И затем, когда обещание разрешится и пользователь ответит на вопрос, происходит возобновление приостановленного метода – восстанавливаются локальные переменные, и мы можем продолжить работу.
Фактически асинхронность осталась абсолютно такой же, какой она была при использовании callback’ов. Изменился только синтаксис. Это такой синтаксический сахар для облегчения восприятия программного кода. Но асинхронность абсолютно та же.
Чтобы убедиться в этом, можно выполнить этот программный код.
Здесь вывод сообщения «Пользователь выбрал вариант» произойдет до того, как пользователь ответит на вопрос.
Если бы здесь реально было ожидание, мы бы сначала дождались, пока пользователь ответит, а только потом вывели бы эту строку на экран.
Но здесь все работает не так – сначала произойдет вывод сообщения, и только потом мы узнаем выбранный пользователем вариант.
В чем принципиальное отличие длительных операций с использованием фоновых заданий и новых асинхронных методов?
Напомню, что все, о чем была речь до сих пор, касалось клиентских асинхронных методов.
Но когда мы говорим про асинхронность, нужно обязательно задавать вопрос: «Какая асинхронность имеется в виду?» Потому что, например, привычная всем картинка с зевающим котиком – это тоже про асинхронность. Ведь когда мы заполняем какой-то большой документ или формируем отчет, пользовательский интерфейс не блокируется. Пользователь дальше может формировать другой отчет, работать с другими документами.
Это тоже асинхронность, но она вообще другая – она реализована при помощи подсистемы «Длительные операции» библиотеки БСП. И работает с помощью фоновых заданий, которые запускаются на сервере.
Механизм длительных операций реализован следующим образом:
-
На форме документа есть реквизит ИдентификаторЗадания.
-
Мы запускаем фоновое задание – фактически это отдельный сеанс, который работает на сервере. И периодически опрашиваем, выполнено ли задание? Если нет, то ждем. Снова спрашиваем: “Уже выполнилось?“
-
И когда задание выполнилось, мы можем забрать его результат из временного хранилища, чтобы воспользоваться им для заполнения документа.
Эти два момента обязательно нужно различать – это всё асинхронность, но принципиально разная.
Изучите возможности платформы 1С и применяйте асинхронные методы правильно
Давайте подведем итог.
-
Асинхронные методы появились, потому что фирме «1С» было важно поддерживать веб-клиент, который исполняется в браузере. Браузер нам диктует, как нужно работать. Фирма «1С» не стала прятать эту асинхронность под капот и переложила заботы по адаптации кода на плечи программистов.
-
Для асинхронного выполнения кода у нас есть два варианта – это использование либо callback’ов, либо Обещаний. Чтобы использовать Обещания, нужна как минимум платформа 8.3.18.
-
У использования callback’ов есть существенный минус – в этом случае код становится очень фрагментированным, сложным для восприятия, отладки, прочтения, доработки.
-
Использование Обещаний делает код похожим на использование обычных синхронных методов. Он становится логически понятнее, его проще изучать, дорабатывать, отлаживать. Но нужно понимать, что асинхронность сохраняется, она осталась та же самая. Изменился только синтаксис – внешний вариант, как мы с этим работаем.
Важно отметить, что в типовых конфигурациях Обещания еще не используются (прим. ред. ноябрь 2021 года). Но ожидается, что в скором времени эта функциональность будет использоваться повсеместно.
Начните изучать этот момент – подготовьте какие-то свои примеры, пройдитесь отладчиком, посмотрите, как это все работает.
И когда в типовых все это появится, мы будем уже готовы – будем знать, что конкретно нужно делать, как отлаживать, как работать с этим и как дорабатывать.
Вопросы
Мы можем создавать свои функции с модификатором «Асинх» и вызывать их по оператору «Ждать». Но никакой магии асинхронности при этом не происходит. Я не могу вызвать кучу функций, которые, например, гоняют на клиенте пустые циклы. Получается, что все это сделано только для интерактивных действий типа выбора файла?
Да, вы все правильно говорите. Те функции с Асинх, которые я приводил в презентации, – это платформенные функции. Чтобы их использовать, мы добавляем модификатор «Асинх» в своей функции, используем оператор «Ждать» и вызываем типовую функцию, которая реализована в платформе. Это сделано именно для таких сценариев.
Произвольный, полностью асинхронный клиентский код мы сделать не можем – мы ограничены наборами, которые нам предлагает платформа.
Чем асинхронные вызовы отличаются от модальных?
Модальные методы отображают модальные окна, которые требуют от пользователя ввода информации, без которой дальнейшая работа алгоритма невозможна. Это, например, методы Вопрос, ОткрытьФорму и т.д.
А есть другие методы, которые не связаны с открытием модальных окон, например, КопироватьФайл. Вот для таких методов появился асинхронный аналог - КопироватьФайлАсинх.
Поэтому можно сказать, что асинхронные методы - это более широкий класс методов, чем модальные.
В асинхронном коде мы вызвали метод, инициировали показ вопроса, но код пошел выполняться дальше. Именно поэтому чаще всего после вызова асинхронного метода с ОписаниемОповещения текст процедуры или функции обрывается.
В синхронном коде – дальнейший код будет выполнен, когда пользователь ответит.
Асинхронный код не блокирует поток выполнения команд. В случае, если мы используем Обещания, там используется приостановка – мы в нее вернемся, и для этого платформа сохраняет локальные переменные, которые могут использоваться в стеке. Произойдет возобновление, и код будет выполняться дальше.
Но, фактически, в момент приостановки произойдет выход из процедуры – мы не остановимся там, где использовали Ждать, а вернемся в предыдущую процедуру. Вот такая особенность.
*************
Статья написана по итогам доклада (видео), прочитанного на конференции Infostart Event.