Появление долгожданного средства, аналогичного Async/Await в других языках, дало возможность существенно улучшить написание кода.
С целью изучения особенностей работы асинхронных функций был поставлен ряд экспериментов.
Эксперимент 1. Собственные асинхронные функции
Создадим 4 асинхронных функции, так, чтобы они вызывались каскадно:
Эксперимент1(Команда) // Функция0
- Функция1()
- Функция2()
- Функция3()
- Функция4()
Вызов каждой функции сделаем опционально: с ключевым словом Ждать и без него (в зависимости от соответствующего флажка). Будем фиксировать время, прошедшее от старта эксперимента, в следующих точках: начало выполнения функции (оно же вызов вложенной функции), начало выполнения кода после вызова вложенной функции и завершение выполнения. Тело функции - простая задержка на определенное количество секунд. Для наших функций выберем задержки 1, 2, 4, 8, 16. Надеюсь, всем понятно, почему выбран ряд степеней двойки?:) Код получившихся функций имеет вид:
&НаКлиенте
Асинх Функция Функция1()
ДобавитьВЛог("Функция1: Старт");
Если Ждать2 Тогда
Ответ2 = Ждать Функция2();
Иначе
Ответ2 = Функция2();
КонецЕсли;
ДобавитьВЛог("Функция1: После вызова Функция2");
Задержка(2);
ДобавитьВЛог("Функция1: Финиш");
Возврат 1;
КонецФункции
Результаты будем выводить в список значений на форме. Почему не в окно сообщений? Потому что, как показал эксперимент, при работе асинхронных функций порядок вывода сообщений с помощью Сообщить() не соответствует ожидаемому. Дополнительно выведем на форму значения переменных ОтветN - результатов возврата соответствующих функций ФункцияN. Кроме этого создадим обработчик ожидания, который каждую секунду будет выводить на форму текущее время.
Итак, начнем.
Эксперимент 1.1
Запустим наши асинхронные функции без использования Ждать
Сразу отметим, что во время выполнения пользовательский интерфейс блокируется, текущее время на форме не обновляется, т.е. обработчик ожидания не срабатывает.
Через полминуты получим результат:
Функция0: Старт :: 0сек.
Функция1: Старт :: 0сек.
Функция2: Старт :: 0сек.
Функция3: Старт :: 0сек.
Функция4: Старт :: 0сек.
Функция4: Финиш :: 16сек.
Функция3: После вызова Функция4 :: 16сек.
Функция3: Финиш :: 24сек.
Функция2: После вызова Функция3 :: 24сек.
Функция2: Финиш :: 28сек.
Функция1: После вызова Функция2 :: 28сек.
Функция1: Финиш :: 30сек.
Функция0: После вызова Функция1 :: 30сек.
Функция0: Финиш :: 31сек.
Ответ1: Обещание
Ответ2: Обещание
Ответ3: Обещание
Ответ4: Обещание
Как видим последовательность выполнения кода точно такая же, как и в случае, когда функции не асинхронные.
Вывод: работа асинхронных функций без ключевого слова Ждать ничем не отличается от работы обычных функций, разве что возвращаемое значение - Обещание.
Эксперимент 1.2
Сделаем теперь вызов Функция3 с помощью Ждать.
Результат:
Функция1: Старт :: 0сек.
Функция2: Старт :: 0сек.
Функция3: Старт :: 0сек.
Функция4: Старт :: 0сек.
Функция4: Финиш :: 16сек.
Функция3: После вызова Функция4 :: 16сек.
Функция3: Финиш :: 24сек.
Функция1: После вызова Функция2 :: 24сек.
Функция1: Финиш :: 26сек.
Функция0: После вызова Функция1 :: 26сек.
Функция0: Финиш :: 27сек.
Функция2: После вызова Функция3 :: 27сек.
Функция2: Финиш :: 31сек.
Функция0: Старт :: 0сек.
Ответ1: Обещание
Ответ2: Обещание
Ответ3: 1
Ответ4: Обещание
Что мы видим: после старта Функции2, запускается Функция3 и Функция4, они полностью отрабатывают, после этого работа Функции2 прерывается (несмотря на то что Функция3 полностью завершилась и результат уже готов), управление передается Функции1. Функция2 заканчивает свою работу после окончании работы всех вышестоящих функций. На первый взгляд выглядит это довольно странно, хотя определенная логика в этом есть: Ждать применяется не к функции, а к результату функции, т.е. вместо
Ответ3 = Ждать Функция3();
можно написать
Результат = Функция3();
Ответ3 = Ждать Результат;
и тогда становится понятно, почему Функция3 выполняется полностью.
Вывод: Ждать не влияет на процесс выполнения вызываемой функции. Он влияет только на порядок выполнения кода вызывающей функции после Ждать, вне зависимости от готовности результата вызванной функции.
Эксперимент 1.3
Добавим теперь Ждать к вызову Функции2 и Функции4
и получим результат:
Функция0: Старт :: 0сек.
Функция1: Старт :: 0сек.
Функция2: Старт :: 0сек.
Функция3: Старт :: 0сек.
Функция4: Старт :: 0сек.
Функция4: Финиш :: 16сек.
Функция2: После вызова Функция3 :: 16сек.
Функция2: Финиш :: 20сек.
Функция0: После вызова Функция1 :: 20сек.
Функция0: Финиш :: 21сек.
Функция3: После вызова Функция4 :: 21сек.
Функция3: Финиш :: 29сек.
Функция1: После вызова Функция2 :: 29сек.
Функция1: Финиш :: 31сек.
Ответ1: Обещание
Ответ2: 1
Ответ3: Обещание
Ответ4: 1
Как видим, отработала Функция4, Функция3 приостановилась после возврата из Функции4, завершилась Функция2, Функция1 приостановилась после возврата из Функции2, завершился обработчик нажатия кнопки. После этого продолжилось выполнение Функции3 и Функции1. Так же, как и в предыдущих случаях блокировался пользовательский интерфейс и не выполнялся обработчик ожидания.
Вывод: При каскадном выполнении все функции, содержащие Ждать, приостанавливают свою работу, пока не выполнятся все вышестоящие функции. Приостановленное выполнение продолжится в том же порядке, в котором было остановлено.
Итоговый вывод: Использование собственных асинхронных функций (без асинхронных функций платформы) выглядит нецелесообразным. Единственный эффект, которого можем достичь, - изменение порядка выполнения кода, что приносит мало пользы, но может затруднить понимание кода и его отладку.
Идем дальше. Может с применением платформенных асинхронных функций будет получше?
Эксперимент 2. Интерактивные функции платформы
На замену функциям модального режима(напр. Предупреждение) и функциям немодального режима (напр. ПоказатьПредупреждение) пришли асинхронные функции (напр. ПредупреждениеАсинх). Для обеспечения взаимодействия с пользователем кроме ПредупреждениеАсинх появились также ВопросАсинх и ОткрытьЗначениеАсинх. Проведем эксперимент №2.
&НаКлиенте
Процедура Эксперимент2(Команда)
НачалоЭксперимента();
ДобавитьВЛог("Проц: Старт");
ИнтерактивныеФункции();
ДобавитьВЛог("Проц: После вызова ИнтерактивныеФункции");
ЗапускОжиданияОтвета(1);
ЗапускОжиданияОтвета(2);
ЗапускОжиданияОтвета(3);
ДобавитьВЛог("Проц: Финиш");
КонецПроцедуры
&НаКлиенте
Асинх Процедура ИнтерактивныеФункции()
ДатаСтарта = ТекущаяДата();
ДобавитьВЛог("ИнтерактивныеФункции: Старт");
Ответ1 = ПредупреждениеАсинх("Предупреждение 1");
ДобавитьВЛог("ИнтерактивныеФункции: После вызова ПредупреждениеАсинх");
Ответ2 = ОткрытьЗначениеАсинх("Значение 2");
ДобавитьВЛог("ИнтерактивныеФункции: После вызова ОткрытьЗначениеАсинх");
Ответ3 = ВопросАсинх("Вопрос 3", РежимДиалогаВопрос.ДаНет);
ДобавитьВЛог("ИнтерактивныеФункции: После вызова ВопросАсинх");
ДобавитьВЛог("ИнтерактивныеФункции: Финиш");
КонецПроцедуры
&НаКлиенте
Асинх Процедура ЗапускОжиданияОтвета(Номер)
ДобавитьВЛог("ЗапускОжиданияОтвета " + Номер + ": Старт");
Если Номер = 1 Тогда Ответ1 = Ждать Ответ1;
ИначеЕсли Номер = 2 Тогда Ответ2 = Ждать Ответ2;
ИначеЕсли Номер = 3 Тогда Ответ3 = Ждать Ответ3;
ИначеЕсли Номер = 4 Тогда Ответ4 = Ждать Ответ4;
КонецЕсли;
ДобавитьВЛог("ЗапускОжиданияОтвета " + Номер + ": Финиш");
КонецПроцедуры
Код выше последовательно выполняет ПредупреждениеАсинх, ОткрытьЗначениеАсинх и ВопросАсинх. После этого запускается ожидание результата каждого вызова.
Результат на экране:
Наконец-то, в отличие от первого эксперимента, получилось что-то похожее на параллельность: запустились все 3 асинхронные функции без каких-либо задержек между ними, и (главное!) для каждой запустили независимое ожидание ответа. Пока система ждет реакции пользователя, каждую секунду отрабатывает обработчик ожидания, т.е. "параллельно" с раздумьями пользователя может выполняться какой-либо код. Пока пользователь не дал свой ответ, все результаты функций - Обещание.
Последовательно закроем все диалоговые окна и получим:
Обещание каждой функции превратилось в какое-либо значение. Каждая процедура ЗапускОжиданияОтвета завершилась отдельно от других — то есть система сохраняла контекст каждой функции до тех пор, пока пользователь не сделал свой выбор. К сожалению, каждое диалоговое окно блокировало интерфейс, включая предыдущие диалоговые окна, поэтому а) нельзя посмотреть, что будет, если закрывать окна в произвольном порядке, б) IRL для пользователя процесс будет выглядеть последовательным: последовательно открылись диалоговые окна, в обратном порядке их последовательно закрыли.
Вывод: Использование асинхронных интерактивных функций позволяет распараллелить как минимум два процесса: ожидание реакции пользователя на диалоговое окно и выполнение кода в обработчике (обработчиках) ожидания. При этом сохраняется возможность получить и обработать ответ пользователя. Так как диалоговые окна блокируют остальной интерфейс, то пользователь не может совершать другие действия во время ожидания.
С интерактивными функциями все понятно, а что с файловыми?
Эксперимент 3. Функции работы с файлами
Многие функции для работы с файлами получили свои асинхронные аналоги. Потестируем КопироватьФайлАсинх, так как она может выполняться достаточно долго для анализа.
&НаКлиенте
Асинх Процедура Эксперимент3(Команда)
НачалоЭксперимента();
ДобавитьВЛог("Эксперимент 3: Старт");
ФайловыеФункции();
ДобавитьВЛог("Эксперимент 3: После вызова ФайловыеФункции");
ЗапускОжиданияОтвета(1);
ЗапускОжиданияОтвета(2);
ДобавитьВЛог("Эксперимент 3: Финиш");
КонецПроцедуры
&НаКлиенте
Асинх Процедура ФайловыеФункции()
НачалоЭксперимента();
ДобавитьВЛог("ФайловыеФункции: Старт");
Ответ1 = КопироватьФайлАсинх("D:\1\BigFile1", "D:\1\BigFile1Copy");
ДобавитьВЛог("ФайловыеФункции: После вызова копирования 1");
Ответ2 = КопироватьФайлАсинх("D:\1\BigFile2", "D:\1\BigFile2Copy");
ДобавитьВЛог("ФайловыеФункции: После вызова копирования 2");
ДобавитьВЛог("ФайловыеФункции: Финиш");
КонецПроцедуры
В третьем эксперименте сделаем программно копии файлов BigFile1 (размер ~4,2 Гб) и BigFile2 (размер ~1,4 Гб).
Запускаем тест и... на несколько минут вся система замирает: недоступен интерфейс, ничего не отражается в логе, не запускается обработчик ожидания.
Только спустя некоторое время мы видим:
Это совсем не то, что ожидалось. Понаблюдав за создаваемыми в каталоге файлами, приходим к выводу:
Вывод: Несмотря на то, что функции КопироватьФайлАсинх возвращают Обещание сразу, копирование каждого файла начинается после завершения копирования предыдущего. При этом Обещание превращается в результат одновременно для всех функций после завершения копирования последнего файла. В процессе копирования работа системы блокируется.
Может передача файлов на сервер будет работать лучше?
Эксперимент 4. Функции обмена файлами с сервером
&НаКлиенте
Процедура Эксперимент4(Команда)
НачалоЭксперимента();
ДобавитьВЛог("Эксперимент 4: Старт");
ФайловыеФункции2();
ДобавитьВЛог("Эксперимент 4: После вызова ФайловыеФункции2");
ЗапускОжиданияОтвета(1);
ЗапускОжиданияОтвета(2);
ДобавитьВЛог("Эксперимент 4: Финиш");
КонецПроцедуры
&НаКлиенте
Асинх Процедура ФайловыеФункции2()
НачалоЭксперимента();
ДобавитьВЛог("ФайловыеФункции2: Старт");
Ответ1 = ПоместитьФайлНаСерверАсинх(,,, "D:\1\File1");
ДобавитьВЛог("ФайловыеФункции2: После вызова помещения файла 1");
Ответ2 = ПоместитьФайлНаСерверАсинх(,,, "D:\1\File2");
ДобавитьВЛог("ФайловыеФункции2: После вызова помещения файла 2");
ДобавитьВЛог("ФайловыеФункции2: Финиш");
КонецПроцедуры
Здесь мы помещаем на сервер File1 (размер 56 Мб) и File2 (размер 23 Мб).
Итак запускаем...
Бинго! Все работает так, как нам бы хотелось:
Сразу после запуска:
через несколько секунд:
и в конце:
Таким образом передача 2 файлов на сервер начинается и заканчивается независимо друг от друга, время завершения операции передачи каждого файла коррелирует с его размером, то есть мы можем утверждать, что передаются они параллельно. При этом, во время передачи не блокируется интерфейс, пользователь может продолжать работать в системе, открывать другие окна и т.п.
Вывод: Асинхронные функции передачи файлов на сервер работают независимо друг от друга, выполняются параллельно и не мешают действиям пользователя. Ожидания результатов функций с помощью Ждать также независимы друг от друга и выполняются с сохранением контеста каждой процедуры, в которой они происходят.
На этом наши эксперименты завершены. Все эксперименты проводились в клиент-серверном режиме, в тонком клиенте, на платформе 8.3.18.1363. В следующих версиях поведение платформы может измениться. За рамками исследования остался веб-клиент, функции вроде ПодключитьРасширениеРаботыСФайламиАсинх, ЗапуститьПриложениеАсинх и т.п., асинхронные методы объектов МенеджерФайловыхПотоков, ТабличныйДокумент и других. Желающие могут продолжить эксперименты самостоятельно, обработка приложена к статье.
Хотелось бы отметить, что несмотря на все преимущества методики Асинх/Ждать не стоит отказываться от функций, использующих ОписаниеОповещения. В ряде случаев применение ОписаниеОповещения предпочтительнее. Во-первых: когда различные исходы функции требуют различной обработки и/или в процессе выполнения надо дать возможность вызывать пользовательский код - можно передать в параметрах несколько обработчиков. Для примера можно привести функцию ПоместитьФайлНаСерверАсинх(<ОписаниеОповещенияОХодеВыполнения>, <ОписаниеОповещенияПередНачалом>, <Адрес>, <ПутьКФайлу>, <УникальныйИдентификаторФормы>). Здесь совмещены оба подхода - Асинх/Ждать и ОписаниеОповещения. Во-вторых: ОписаниеОповещения можно передать параметром внутрь каскада выполняющихся функций, для того, чтобы какая-то из них (а может и не одна), имела возможность вернуть свой результат в вызывающий модуль. Тем самым это напоминает передачу указателя на функцию в некоторых языках. Преимуществом же Асинх/Ждать является большая наглядность, сокращение объема кода и полный доступ к контексту выполнения вызывающего кода после завершения асинхронной функции (напомню, что при использовании ОписаниеОповещения необходимый контекст надо передать в ДополнительныеПараметры).
И напоследок, пара ложек дегтя. Первая ложка: отсутствует функция ОткрытьФормуАсинх. В моих проектах бОльшая часть случаев использования ОписаниеОповещения относится к открытию форм и обработке результатов закрытия. Вторая ложка: почему нельзя определить, что находится внутри Обещание без использования Ждать? Ведь Ждать вызывает приостановку выполнения кода, а это может быть вовсе не нужно. Надеемся, в будущем это будет доработано.
PS: Как всегда, в комментариях приветствуются замечания, дополнения и сообщения об ошибках.
PPS: А что же все-таки значит в заголовке "... Асинхронное исследование ..."?