Как распараллелить выполнение длительной процедуры с помощью фоновых заданий в 1С?

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

При интеграции с внешними сервисами, загрузке прайс-листов и обработке больших массивов данных последовательное выполнение алгоритмов в одном потоке может занимать часы. Рассмотрим, как оптимизировать тяжелые серверные процессы, организовав параллельную обработку в несколько потоков через фоновые задания, а также разберем типичные ошибки и нюансы платформы 1С:Предприятие.

Анализ исходной задачи

Представим типовой сценарий: необходимо запросить цены по массиву номенклатуры у множества внешних поставщиков через API. В исходном варианте код обходит список поставщиков в цикле последовательно:


Для Каждого Поставщик Из СписокПоставщиков Цикл
    Выгрузка = ПодключениеИЗагрузкаДанныхПоставщиков.ПолучитьНастройкиAPIПоставщика(Поставщик);
    Если Выгрузка.Количество() = 0 Тогда
        Возврат;
    КонецЕсли;

    // Выполнение запросов к API поставщика
    Рез = ВыполнитьЗапрос(0, Выгрузка, ..., ТЗ, ПакетнаяОбработка);
    // ... дальнейшие итерации
КонецЦикла;

Основное узкое место такого подхода — сетевое ожидание (I/O wait). Пока отправляется запрос к одному поставщику, система простаивает в ожидании HTTP-ответа. Если поставщиков десятки, общее время работы возрастает многократно. Оптимальный выход — распределить работу по независимым потокам.

Вариант 1. Использование механизма длительных операций БСП

Если конфигурация разработана на базе Библиотеки стандартных подсистем (БСП), самым надежным и стандартизированным способом является модуль ДлительныеОперации. Разберем по шагам, как строится архитектура фонового процесса с получением результата:

  1. Экспортная процедура в серверном модуле или модуле менеджера: процедура принимает параметры и адрес во временном хранилище, куда в конце работы помещает итог.
    
    Процедура ВыполнитьДействие(Параметры, АдресРезультата) Экспорт
        // Выполнение логики получения данных
        Результат = Новый ТаблицаЗначений;
        // ... заполнение таблицы
        ПоместитьВоВременноеХранилище(Результат, АдресРезультата);
    КонецПроцедуры
    
  2. Запуск операции и контроль ожидания на клиенте: инициализация фонового потока и запуск периодической проверки статуса.
    
    &НаКлиенте
    Процедура ВыполнитьДействие()
        ДлительнаяОперация = НачатьВыполнениеНаСервере();
        ПараметрыОжидания = ДлительныеОперацииКлиент.ПараметрыОжидания(ЭтотОбъект);
        ОповещениеОЗавершении = Новый ОписаниеОповещения("ВыполнитьДействиеЗавершение", ЭтотОбъект);
        ДлительныеОперацииКлиент.ОжидатьЗавершение(ДлительнаяОперация, ОповещениеОЗавершении, ПараметрыОжидания);
    КонецПроцедуры
    
    &НаСервере
    Функция НачатьВыполнениеНаСервере()
        ПараметрыПроцедуры = Новый Структура;
        ПараметрыВыполнения = ДлительныеОперации.ПараметрыВыполненияВФоне(УникальныйИдентификатор);
        Возврат ДлительныеОперации.ВыполнитьВФоне("Обработка.МояОбработка.ВыполнитьДействие", ПараметрыПроцедуры, ПараметрыВыполнения);
    КонецФункции
    
  3. Обработка завершения фонового процесса:
    
    &НаКлиенте
    Процедура ВыполнитьДействиеЗавершение(Результат, ДополнительныеПараметры) Экспорт
        Если Результат = Неопределено Тогда
            Возврат;
        КонецЕсли;
        
        Если Результат.Статус = "Ошибка" Тогда
            СтандартныеПодсистемыКлиент.ВывестиИнформациюОбОшибке(Результат.ИнформацияОбОшибке);
            Возврат;
        КонецЕсли;
        
        // Получение сохраненных данных по адресу Результат.АдресРезультата
        ВывестиРезультат(Результат);
    КонецПроцедуры
    

Вариант 2. Прямой запуск пула фоновых заданий через менеджер платформы

Если требуется запустить сразу фиксированное количество параллельных потоков напрямую на сервере (например, 4–5 заданий), используют метод ФоновыеЗадания.Выполнить(). Посмотрим на практическую реализацию пакетного запуска:


Процедура ЗагрузитьЦеныПоставщиковЭксп_Многопоток(Список, ДанныеПоставщика, ПакетнаяОбработка = Ложь) Экспорт

    Если Список.Количество() = 0 Тогда
        Возврат;
    КонецЕсли;

    СписокПоставщиков = Новый Массив;
    НомераПоставщиков = Новый Массив;

    Для Каждого Поставщик Из Список Цикл
        Масс = ПодключениеИЗагрузкаДанныхПоставщиков.СтрРазделить(Поставщик, "~", Истина);
        НомераПоставщиков.Добавить(Масс[0]);
        СписокПоставщиков.Добавить(Масс[1]);
    КонецЦикла;

    ЧислоПараллельныхПотоков = 5;
    МассивЗаданий = Новый Массив;

    Для Каждого Поставщик Из СписокПоставщиков Цикл
        
        МассивПараметров = Новый Массив;
        МассивПараметров.Добавить(Поставщик);
        МассивПараметров.Добавить(ПакетнаяОбработка);
        МассивПараметров.Добавить(ДанныеПоставщика);
        МассивПараметров.Добавить(СписокПоставщиков);
        МассивПараметров.Добавить(Список);
        МассивПараметров.Добавить(НомераПоставщиков);

        // Запуск отдельного фонового задания
        Задание = ФоновыеЗадания.Выполнить("ПодключениеИЗагрузкаДанныхПоставщиков.ЗагрузитьДанныеПоставщика", МассивПараметров);
        МассивЗаданий.Добавить(Задание);

        // Если пул заполнен, ожидаем завершения пачки
        Если МассивЗаданий.Количество() >= ЧислоПараллельныхПотоков Тогда
            Попытка
                ФоновыеЗадания.ОжидатьЗавершения(МассивЗаданий);
            Исключение
                // Обязательно логируем ошибку, не оставляем блок пустым
                ЗаписьЖурналаРегистрации("ФоновыеЗадания", УровеньЖурналаРегистрации.Ошибка,,, ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));
            КонецПопытки;
            МассивЗаданий.Очистить();
        КонецЕсли;

    КонецЦикла;

    // Ожидание оставшихся заданий в «хвосте»
    Если МассивЗаданий.Количество() > 0 Тогда
        Попытка
            ФоновыеЗадания.ОжидатьЗавершения(МассивЗаданий);
        Исключение
            ЗаписьЖурналаРегистрации("ФоновыеЗадания", УровеньЖурналаРегистрации.Ошибка,,, ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));
        КонецПопытки;
        МассивЗаданий.Очистить();
    КонецЕсли;

КонецПроцедуры

Подводные камни и важные особенности реализации

Проанализируем ключевые моменты, о которых важно помнить при параллелизации в 1С:

  1. Изоляция сеансов и возврат данных: фоновое задание выполняется в совершенно отдельном сеансе. Объекты, переданные в качестве параметров (например, ТаблицаЗначений), передаются по значению через сериализацию. Модифицировать таблицу внутри задания так, чтобы изменения отразились в родительском потоке, невозможно. Возвращать данные следует через ПоместитьВоВременноеХранилище() или путем записи во временные таблицы/регистры сведений.
  2. Проблема ожидания пачками: метод ФоновыеЗадания.ОжидатьЗавершения(МассивЗаданий) блокирует процесс до тех пор, пока не завершится каждый поток из переданного списка. Если 4 потока отработали за 1 секунду, а один «завис» на 30 секунд, остальные ядра будут простаивать. Для максимальной производительности предпочтительнее стратегия «скользящего окна»: как только один поток завершился, на его место сразу запускается следующий.
  3. Обработка исключений: пустой блок Исключение ... КонецПопытки крайне опасен. При сбое одного задания вызывающий код продолжит работу, потеряв часть данных и не уведомив пользователя. Всегда проверяйте статус заданий через Задание.Состояние = СостояниеФоновогоЗадания.Ошибка и фиксируйте свойство Задание.ИнформацияОбОшибке.
  4. Файловый режим базы: в файловой информационной базе платформа 1С выполняет фоновые задания последовательно одно за другим. Полноценная параллельная работа поддерживается исключительно в клиент-серверном варианте (SQL).
  5. Лимиты внешних API и таймауты: при параллельных HTTP-запросах серверы поставщиков могут ограничивать частоту обращений (Rate Limiting) и возвращать ошибку с кодом 429. Обязательно задавайте корректные таймауты в объекте HTTPСоединение и закладывайте обработку повторных попыток при сбоях сети.
← На главную