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