При интеграции с внешними сервисами, загрузке прайс-листов и обработке больших массивов данных последовательное выполнение алгоритмов в одном потоке может занимать часы. Рассмотрим, как оптимизировать тяжелые серверные процессы, организовав параллельную обработку в несколько потоков через фоновые задания, а также разберем типичные ошибки и нюансы платформы 1С:Предприятие.
Представим типовой сценарий: необходимо запросить цены по массиву номенклатуры у множества внешних поставщиков через API. В исходном варианте код обходит список поставщиков в цикле последовательно:
Для Каждого Поставщик Из СписокПоставщиков Цикл
Выгрузка = ПодключениеИЗагрузкаДанныхПоставщиков.ПолучитьНастройкиAPIПоставщика(Поставщик);
Если Выгрузка.Количество() = 0 Тогда
Возврат;
КонецЕсли;
// Выполнение запросов к API поставщика
Рез = ВыполнитьЗапрос(0, Выгрузка, ..., ТЗ, ПакетнаяОбработка);
// ... дальнейшие итерации
КонецЦикла;
Основное узкое место такого подхода — сетевое ожидание (I/O wait). Пока отправляется запрос к одному поставщику, система простаивает в ожидании HTTP-ответа. Если поставщиков десятки, общее время работы возрастает многократно. Оптимальный выход — распределить работу по независимым потокам.
Если конфигурация разработана на базе Библиотеки стандартных подсистем (БСП), самым надежным и стандартизированным способом является модуль ДлительныеОперации. Разберем по шагам, как строится архитектура фонового процесса с получением результата:
Процедура ВыполнитьДействие(Параметры, АдресРезультата) Экспорт
// Выполнение логики получения данных
Результат = Новый ТаблицаЗначений;
// ... заполнение таблицы
ПоместитьВоВременноеХранилище(Результат, АдресРезультата);
КонецПроцедуры
&НаКлиенте
Процедура ВыполнитьДействие()
ДлительнаяОперация = НачатьВыполнениеНаСервере();
ПараметрыОжидания = ДлительныеОперацииКлиент.ПараметрыОжидания(ЭтотОбъект);
ОповещениеОЗавершении = Новый ОписаниеОповещения("ВыполнитьДействиеЗавершение", ЭтотОбъект);
ДлительныеОперацииКлиент.ОжидатьЗавершение(ДлительнаяОперация, ОповещениеОЗавершении, ПараметрыОжидания);
КонецПроцедуры
&НаСервере
Функция НачатьВыполнениеНаСервере()
ПараметрыПроцедуры = Новый Структура;
ПараметрыВыполнения = ДлительныеОперации.ПараметрыВыполненияВФоне(УникальныйИдентификатор);
Возврат ДлительныеОперации.ВыполнитьВФоне("Обработка.МояОбработка.ВыполнитьДействие", ПараметрыПроцедуры, ПараметрыВыполнения);
КонецФункции
&НаКлиенте
Процедура ВыполнитьДействиеЗавершение(Результат, ДополнительныеПараметры) Экспорт
Если Результат = Неопределено Тогда
Возврат;
КонецЕсли;
Если Результат.Статус = "Ошибка" Тогда
СтандартныеПодсистемыКлиент.ВывестиИнформациюОбОшибке(Результат.ИнформацияОбОшибке);
Возврат;
КонецЕсли;
// Получение сохраненных данных по адресу Результат.АдресРезультата
ВывестиРезультат(Результат);
КонецПроцедуры
Если требуется запустить сразу фиксированное количество параллельных потоков напрямую на сервере (например, 4–5 заданий), используют метод ФоновыеЗадания.Выполнить(). Посмотрим на практическую реализацию пакетного запуска:
Процедура ЗагрузитьЦеныПоставщиковЭксп_Многопоток(Список, ДанныеПоставщика, ПакетнаяОбработка = Ложь) Экспорт
Если Список.Количество() = 0 Тогда
Возврат;
КонецЕсли;
СписокПоставщиков = Новый Массив;
НомераПоставщиков = Новый Массив;
Для Каждого Поставщик Из Список Цикл
Масс = ПодключениеИЗагрузкаДанныхПоставщиков.СтрРазделить(Поставщик, "~", Истина);
НомераПоставщиков.Добавить(Масс[0]);
СписокПоставщиков.Добавить(Масс[1]);
КонецЦикла;
ЧислоПараллельныхПотоков = 5;
МассивЗаданий = Новый Массив;
Для Каждого Поставщик Из СписокПоставщиков Цикл
МассивПараметров = Новый Массив;
МассивПараметров.Добавить(Поставщик);
МассивПараметров.Добавить(ПакетнаяОбработка);
МассивПараметров.Добавить(ДанныеПоставщика);
МассивПараметров.Добавить(СписокПоставщиков);
МассивПараметров.Добавить(Список);
МассивПараметров.Добавить(НомераПоставщиков);
// Запуск отдельного фонового задания
Задание = ФоновыеЗадания.Выполнить("ПодключениеИЗагрузкаДанныхПоставщиков.ЗагрузитьДанныеПоставщика", МассивПараметров);
МассивЗаданий.Добавить(Задание);
// Если пул заполнен, ожидаем завершения пачки
Если МассивЗаданий.Количество() >= ЧислоПараллельныхПотоков Тогда
Попытка
ФоновыеЗадания.ОжидатьЗавершения(МассивЗаданий);
Исключение
// Обязательно логируем ошибку, не оставляем блок пустым
ЗаписьЖурналаРегистрации("ФоновыеЗадания", УровеньЖурналаРегистрации.Ошибка,,, ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));
КонецПопытки;
МассивЗаданий.Очистить();
КонецЕсли;
КонецЦикла;
// Ожидание оставшихся заданий в «хвосте»
Если МассивЗаданий.Количество() > 0 Тогда
Попытка
ФоновыеЗадания.ОжидатьЗавершения(МассивЗаданий);
Исключение
ЗаписьЖурналаРегистрации("ФоновыеЗадания", УровеньЖурналаРегистрации.Ошибка,,, ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));
КонецПопытки;
МассивЗаданий.Очистить();
КонецЕсли;
КонецПроцедуры
Проанализируем ключевые моменты, о которых важно помнить при параллелизации в 1С:
ТаблицаЗначений), передаются по значению через сериализацию. Модифицировать таблицу внутри задания так, чтобы изменения отразились в родительском потоке, невозможно. Возвращать данные следует через ПоместитьВоВременноеХранилище() или путем записи во временные таблицы/регистры сведений.ФоновыеЗадания.ОжидатьЗавершения(МассивЗаданий) блокирует процесс до тех пор, пока не завершится каждый поток из переданного списка. Если 4 потока отработали за 1 секунду, а один «завис» на 30 секунд, остальные ядра будут простаивать. Для максимальной производительности предпочтительнее стратегия «скользящего окна»: как только один поток завершился, на его место сразу запускается следующий.Исключение ... КонецПопытки крайне опасен. При сбое одного задания вызывающий код продолжит работу, потеряв часть данных и не уведомив пользователя. Всегда проверяйте статус заданий через Задание.Состояние = СостояниеФоновогоЗадания.Ошибка и фиксируйте свойство Задание.ИнформацияОбОшибке.HTTPСоединение и закладывайте обработку повторных попыток при сбоях сети.