При разработке высокопроизводительных систем на платформе 1С часто возникает необходимость распараллеливания тяжелых задач. Когда мы запускаем n фоновых заданий одновременно, пользователь хочет видеть не просто «крутилку» ожидания, а конкретный прогресс выполнения всей операции. Однако стандартный механизм ДлительныеОперации в простейшем виде рассчитан на одно задание. В этой статье мы подробно разберем, как объединить данные от нескольких независимых потоков и вывести их на общую полосу индикатора.
Представим ситуацию: мы обрабатываем большой массив данных (например, 100 000 строк), который разбит на 10 параллельных потоков по 10 000 строк. Нам нужно, чтобы индикатор на форме показывал общее состояние дел: если первое задание выполнило 5 000 строк, а второе — 3 000, общий прогресс должен составлять 8%. Проблема заключается в том, что фоновые задания работают в разных сеансах и не имеют прямого доступа к контексту формы. Нам необходимо промежуточное хранилище и механизм опроса этого хранилища.
Рассмотрим основные технические подходы к решению этой задачи:
Этот метод подходит для любых версий платформы. Разберем его по шагам. Создадим непериодический регистр сведений ПрогрессФоновыхОпераций с измерениями ИдентификаторЗадания (Тип: УникальныйИдентификатор) и ресурсом ОбработаноСтрок (Тип: Число).
Проанализируем алгоритм работы внутри фонового задания. В цикле обработки данных задание должно периодически обновлять свою запись в регистре:
Процедура ВыполнитьОбработкуДанных(ИдентификаторЗадания, ОбщееКоличество) Экспорт
Для Счетчик = 1 По ОбщееКоличество Цикл
// Основная логика обработки
// ...
// Обновляем прогресс каждые 100 строк, чтобы не перегружать СУБД
Если Счетчик % 100 = 0 Тогда
МенеджерЗаписи = РегистрыСведений.ПрогрессФоновыхОпераций.СоздатьМенеджерЗаписи();
МенеджерЗаписи.ИдентификаторЗадания = ИдентификаторЗадания;
МенеджерЗаписи.ОбработаноСтрок = Счетчик;
МенеджерЗаписи.Записать();
КонецЕсли;
КонецЦикла;
КонецПроцедуры
Теперь выясним, как отобразить это на форме. Нам потребуется Обработчик ожидания. При нажатии на кнопку запуска мы подключаем процедуру, которая будет опрашивать регистр и суммировать значения:
&НаКлиенте
Процедура ПодключитьМониторинг()
ПодключитьОбработчикОжидания("ОбновитьИндикаторПрогресса", 1); // Опрос каждую секунду
КонецПроцедуры
&НаСервере
Функция ПолучитьОбщийПрогрессСервер(МассивИдентификаторов)
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| СУММА(Прогресс.ОбработаноСтрок) КАК ВсегоОбработано
|ИЗ
| РегистрСведений.ПрогрессФоновыхОпераций КАК Прогресс
|ГДЕ
| Прогресс.ИдентификаторЗадания В(&МассивИдентификаторов)";
Запрос.УстановитьПараметр("МассивИдентификаторов", МассивИдентификаторов);
Результат = Запрос.Выполнить().Выбрать();
Если Результат.Следующий() Тогда
Возврат Результат.ВсегоОбработано;
КонецЕсли;
Возврат 0;
КонецФункции
Метод с регистром сведений создает лишнюю нагрузку на транзакционный лог. Рассмотрим более элегантный способ — использование метода Сообщить() внутри фонового задания и метода ПолучитьСообщенияПользователю() в основном сеансе.
Выясним причину эффективности этого метода: сообщения не записываются в базу данных, а хранятся в очереди сообщений фонового задания в оперативной памяти сервера. В коде фонового задания мы будем транслировать прогресс в виде JSON-строки с префиксом:
// Внутри фонового задания
ТекстПрогресса = "{""processed"":" + Счетчик + "}";
Сообщить("#PROGRESS#" + ТекстПрогресса);
На стороне формы в обработчике ожидания мы будем собирать эти сообщения для каждого активного задания:
&НаСервере
Функция СобратьДанныеИзСообщений(МассивЗаданий)
ОбщийСчетчик = 0;
Для Каждого Задание Из МассивЗаданий Цикл
ОбъектЗадания = ФоновыеЗадания.НайтиПоУникальномуИдентификатору(Задание);
Если ОбъектЗадания = Неопределено Тогда Продолжить; КонецЕсли;
Сообщения = ОбъектЗадания.ПолучитьСообщенияПользователю(Истина);
Для Каждого Сообщение Из Сообщения Цикл
Если Лев(Сообщение.Текст, 10) = "#PROGRESS#" Тогда
// Парсим JSON и обновляем локальную переменную прогресса для этого задания
// ...
КонецЕсли;
КонецЦикла;
КонецЦикла;
Возврат ОбщийСчетчик;
КонецФункции
Если ваша платформа позволяет использовать РазделяемоеХранилище, это будет самым правильным архитектурным решением. Оно специально предназначено для обмена данными между сеансами без использования СУБД.
Разберем пример записи в хранилище:
// В фоновом задании
РазделяемоеХранилище.Вставить(ИдентификаторЗадания, ТекущийПрогресс);
Основной сеанс просто считывает эти значения по ключам-идентификаторам. Это происходит мгновенно и не блокирует другие процессы.
Проанализируем ситуацию, когда задания выполняются с разной скоростью. Как правильно рассчитать число для ПолеИндикатора? Существует два подхода:
(КоличествоЗавершенных / ОбщееКоличествоЗаданий) * 100.N. Каждое задание сообщает количество обработанных единиц. Общий процент = (СуммаВсехОбработанных / N) * 100.Важный момент: при использовании обработчика ожидания не делайте интервал слишком коротким. Оптимальное время — 1.5–3 секунды. Слишком частые вызовы ПолучитьСообщенияПользователю или запросы к регистру могут замедлить работу самого сервера приложений.
Посмотрим на ситуацию со стороны пользователя. Если общий индикатор замер на 50%, пользователю непонятно, зависла ли вся система или просто одно из десяти заданий обрабатывает сложный кусок данных. Рекомендуем дополнить общую полосу прогресса таблицей со списком текущих заданий, где для каждого выведен свой локальный индикатор. Это повышает прозрачность процесса и доверие пользователя к программе.
Таким образом, мы рассмотрели комплексный подход к реализации общего индикатора. Выбор конкретного метода зависит от версии вашей платформы и требований к производительности, но связка Фоновое задание + Хранилище (Регистр/Сообщения) + Обработчик ожидания на форме является стандартом для решения подобных задач в 1С.