При разработке на платформе 1С:Предприятие 8 программисты часто сталкиваются с задачей выполнения «тяжелых» расчетов, которые не должны мешать работе пользователя. Казалось бы, механизм фоновых заданий идеально подходит для этого, однако на практике часто возникает ситуация: фоновое задание запущено, но управляемая форма все равно блокируется («песочные часы»), а интерфейс не реагирует на действия пользователя до момента завершения расчета. Рассмотрим подробнее, почему это происходит и как реализовать по-настоящему асинхронную работу.
Проанализируем типичную ошибку, представленную в обсуждении. Когда разработчик вызывает серверную процедуру для запуска фонового задания, он часто совершает одну из двух ошибок:
ОжидатьЗавершенияВыполнения() внутри серверного вызова. Этот метод буквально приказывает текущему сеансу (и, соответственно, клиентскому приложению) ждать, пока фоновый поток не закончит работу.Как только мы вызываем серверный метод, основной поток клиентского приложения приостанавливается. Если внутри этого метода мы начинаем «ждать» результат фонового задания, то смысл использования фонового процесса теряется — пользователь все равно видит замерший экран.
Разберем по шагам корректную схему взаимодействия клиента и сервера, которая позволит форме оставаться «живой»:
ФоновыеЗадания.Выполнить() и немедленно возвращаем его уникальный идентификатор (UUID) на клиент, не дожидаясь завершения.UUID.Состояние объекта фонового задания.Рассмотрим пример кода, который реализует данную схему. Сначала подготовим процедуру запуска на клиенте:
&НаКлиенте
Процедура ЗапуститьРасчет(Команда)
// Очищаем предыдущие результаты, если нужно
Объект.РезультатРасчета = "";
// 1. Подключаем проверку статуса (раз в 2 секунды)
ПодключитьОбработчикОжидания("Обработчик_ПроверитьСтатусЗадания", 2);
// 2. Запускаем задание на сервере и получаем ИД
ДанныеЗадания = ЗапуститьЗаданиеНаСервере();
ИдентификаторЗадания = ДанныеЗадания.Идентификатор;
АдресХранилища = ДанныеЗадания.АдресРезультата;
КонецПроцедуры
Теперь проанализируем серверную часть. Важно понимать, что для передачи данных обратно в форму лучше всего использовать ВременноеХранилище, так как фоновое задание работает в отдельном сеансе.
&НаСервере
Функция ЗапуститьЗаданиеНаСервере()
// Генерируем адрес в хранилище, куда задание положит результат
АдресРезультата = ПоместитьВоВременноеХранилище(Неопределено, ЭтаФорма.УникальныйИдентификатор);
МассивПараметров = Новый Массив;
МассивПараметров.Добавить(АдресРезультата); // Передаем адрес как параметр
МассивПараметров.Добавить(Объект.Проект); // Передаем только ССЫЛКИ, а не объекты!
// Запуск фонового задания
// "ДлительныеПроцедуры.ВыполнитьРасчет" - это имя экспортной процедуры в общем модуле
Задание = ФоновыеЗадания.Выполнить("ДлительныеПроцедуры.ВыполнитьРасчет", МассивПараметров, "КлючЗаданияРасчета");
Результат = Новый Структура;
Результат.Вставить("Идентификатор", Задание.УникальныйИдентификатор);
Результат.Вставить("АдресРезультата", АдресРезультата);
Возврат Результат;
КонецФункции
Теперь реализуем процедуру, которую вызывает ПодключитьОбработчикОжидания. Она должна быть максимально «легкой», чтобы пользователь не замечал микро-задержек интерфейса.
&НаКлиенте
Процедура Обработчик_ПроверитьСтатусЗадания() Экспорт
Статус = ПроверитьСостояниеЗаданияНаСервере(ИдентификаторЗадания);
Если Статус.Состояние = "Выполнено" Тогда
ОтключитьОбработчикОжидания("Обработчик_ПроверитьСтатусЗадания");
ПолучитьРезультатИзХранилища();
Сообщить("Расчет завершен успешно!");
ИначеЕсли Статус.Состояние = "Ошибка" Тогда
ОтключитьОбработчикОжидания("Обработчик_ПроверитьСтатусЗадания");
Предупреждение("Ошибка при выполнении: " + Статус.СообщениеОбОшибке);
КонецЕсли;
// Если статус "Выполняется" - ничего не делаем, ждем следующего такта
КонецПроцедуры
&НаСервереБезКонтекста
Функция ПроверитьСостояниеЗаданияНаСервере(ИдЗадания)
Результат = Новый Структура("Состояние, СообщениеОбОшибке", "Выполняется", "");
Задание = ФоновыеЗадания.НайтиПоУникальномуИдентификатору(ИдЗадания);
Если Задание = Неопределено Тогда
Результат.Состояние = "Ошибка";
Результат.СообщениеОбОшибке = "Задание не найдено";
ИначеЕсли Задание.Состояние = СостояниеФоновогоЗадания.Завершено Тогда
Результат.Состояние = "Выполнено";
ИначеЕсли Задание.Состояние = СостояниеФоновогоЗадания.ЗавершеноАварийно Тогда
Результат.Состояние = "Ошибка";
Результат.СообщениеОбОшибке = КраткоеПредставлениеОшибки(Задание.ИнформацияОбОшибке);
ИначеЕсли Задание.Состояние = СостояниеФоновогоЗадания.Отменено Тогда
Результат.Состояние = "Ошибка";
Результат.СообщениеОбОшибке = "Задание было отменено пользователем";
КонецЕсли;
Возврат Результат;
КонецФункции
Разберем несколько критических моментов, которые часто упускают при работе с фоновыми заданиями:
1. Передача объектов в параметры. Никогда не передавайте в фоновое задание ДанныеФормыСтруктура или весь Объект целиком (как это было в сообщении №4 темы). Фоновое задание выполняется в другом сеансе, оно не «видит» вашу форму. Передавайте только Ссылка. Внутри фонового задания объект нужно прочитать из базы данных заново.
2. Использование прогресса. Чтобы пользователь видел, что программа «думает», используйте метод ФоновыеЗадания.СообщитьПрогресс() внутри кода самого задания. В обработчике ожидания на клиенте вы можете считывать эти сообщения через метод ПолучитьСообщенияПользователю() и выводить, например, процент выполнения в заголовок декорации на форме.
3. Временное хранилище. При создании адреса во временном хранилище на сервере (через ПоместитьВоВременноеХранилище) обязательно вторым параметром передавайте ЭтаФорма.УникальныйИдентификатор. Это гарантирует, что данные не «протухнут» до того момента, как фоновое задание их туда запишет, а форма — считает.
4. Права доступа. Помните, что фоновое задание выполняется от имени специального пользователя операционной системы (обычно USR1CV8). Если ваш расчет обращается к файловой системе (например, сетевой шаре, как в примере из темы), убедитесь, что у этого системного пользователя есть соответствующие права доступа к папкам.
Если в вашей конфигурации используется БСП, мы рекомендуем не изобретать велосипед и использовать механизм ДлительныеОперации. Рассмотрим, как это упрощает жизнь:
ДлительныеОперации.ЗапуститьВыполнениеНаСервере() берет на себя создание задания и управление временным хранилищем.ДлительныеОперации.ЗаданиеВыполнено() позволяет одной строчкой проверить статус.ДлительнаяОперация, которая сама покажет красивый индикатор прогресса и кнопку «Отмена» без написания лишнего кода в вашей форме.Таким образом, использование правильной архитектуры «Клиент -> Запуск -> Мгновенный возврат -> Проверка по таймеру» позволяет сделать интерфейс отзывчивым, избавиться от «зависаний» и обеспечить комфортную работу пользователя даже при выполнении самых сложных и длительных расчетов.