Вводная
Представим, что нужно периодически запрашивать какой-то большой объем данных из внешней системы, но повлиять на скорость ответа внешнего сервиса/системы мы не можем, а очень хочется получать данные быстро. При этом открыть несколько параллельных сеансов внешняя система не запрещает. Тогда на помощь приходят они ФоновыеЗадания 1С в серверной базе (в файловой базе выигрыша в скорости не происходит).
Увеличение скорости происходит не линейно и максимально мне удавалось достичь ускорения за счет параллельности максимум в 3-5 раз от скорости в 1 сеансе. Это происходит из-за того, что тратятся ресурсы и время на создание параллельных сеансов, а потом на ожидание и сборку результатов.
Чтобы достичь максимального эффекта для конкретной задачи приходится опытным путем в песочнице определять:
- когда выгодно включать работу через ФоновыеЗадания, а когда можно и через 1 сеанс подождать ответа
- какое количество одновременных сеансов/обращений можно использовать для конкретной таблицы/сервиса.
В 1С теоретически можно создать сколь угодно много фоновых заданий, нО... внешняя система либо может не выдержать такой нагрузки, либо просто забанит нас. Потому рекомендую программно ставить ограничение в 1С не более 10-30 одновременных потоков.
Алгоритм действий:
Объекты метаданных которые понадобятся для решения:
- Общий модуль: Серверный, не глобальный, без использования повторных вызовов, можно, если надо Привилегированный.
- Встроенная или внешняя Обработка, откуда делается вызов общего модуля
- любой РегистрСведений, для примера я взял регистр из УТ 11.4 «ВременныеИдентификаторыЗапросов». Регистр сведений используется как буфер для записи промежуточных результатов фоновыми заданиями и последующей сборки из него.
В общем модуле нам достаточно 1 экспортной процедуры, куда передается на вход сформированный ранее в обработке текст запроса и идентификатор фонового задания для записи промежуточного результата работы процедуры в РегистрСведений с этим же идентификатором.
Общий модуль
ПроизвестиДлительноеВычисление(ПараметрыПроцедуры) Экспорт
Процедура ПроизвестиДлительноеВычисление(ПараметрыПроцедуры) Экспорт
Попытка
СоединениеAdoDb = УстановитьСоединение(ПараметрыПроцедуры.DataSource, ПараметрыПроцедуры.ЭтоЭмуляцияРаботы);
КомандаAdodb = ВернутьКомандаАДОДБ();
КомандаAdodb.CommandText = ПараметрыПроцедуры.ТекстЗапроса;
НачатьТранзакцию();
Если НЕ ПараметрыПроцедуры.ЭтоЭмуляцияРаботы Тогда
КомандаAdodb.ActiveConnection = СоединениеAdoDb;
//упакуем объект ТаблицаЗначений, Для больших таблиц - хороший выигрыш в объеме.
УпакованнаяТЗ = Новый ХранилищеЗначения(ВыгрузитьЗапрос_ТаблЗнач_GetRows(КомандаAdodb.Execute()), Новый СжатиеДанных(9));
Иначе
УпакованнаяТЗ = Новый ХранилищеЗначения(ЗаполнитьТЗМусором(), Новый СжатиеДанных(9));
КонецЕсли;
ПараметрыПроцедуры.Вставить("Запрос", УпакованнаяТЗ);
ЗаписьРС = РегистрыСведений.ВременныеИдентификаторыЗапросов.СоздатьМенеджерЗаписи();
ЗаписьРС.Идентификатор = ПараметрыПроцедуры.Идентификатор;
ЗаписьРС.Прочитать();
ЗаполнитьЗначенияСвойств(ЗаписьРС, ПараметрыПроцедуры);
ЗаписьРС.Записать(Истина);
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
ТекстОшибки = "Фоновый запрос не удался,текст:"+ПараметрыПроцедуры.ТекстЗапроса+"
|Ошибка:"+ОписаниеОшибки();
ВызватьИсключение ТекстОшибки;
КонецПопытки;
КонецПроцедуры
Вспомогательные процедуры/функции и другие выполняющие собственно саму работу процедуры для настоящей статьи не существенны и могут меняться в зависимости от задачи. Имеются в приложенном файле конфигурации.
В обработке запуска фоновых заданий получаем из внешней системы или придумываем свой уникальный идентификатор, по которому будем бить задачи на порции. В примере для ADS берем выборку ключевого поля из нужной таблицы и потом разбиваем полученный результат на структуру, содержащую массивы порционных ключей таблицы.
Если установили ограничение на максимальное количество одновременных фоновых заданий, то в цикле запуска фоновых заданий останавливаемся при достижении лимита, ждем завершения ранее созданных заданий, собираем промежуточные результаты и только потом двигаемся дальше.
Модуль объекта обработки
ВыполнитьЗадачиПараллельно() и СобратьОтветыФоновыхЗаданий
Функция ВыполнитьЗадачиПараллельно() Экспорт
Перем л_ОбщаяТЗ;
Отказ = Ложь;
ПарамФоновых = ПараметрыФоновыхПотоков();
тз_фоновых = ТЗ_Фоновых();
Начало_Транспорта();
КомандаADODB.CommandText = "select DISTINCT "+ИмяКлючевогоПоля+" FROM "+ИмяТаблицы;
Если ЭтоЭмуляцияРаботы Тогда
л_МассивЗнач = ЗаполнитьМассивМусором();
Иначе
л_МассивЗнач = ИмпортПотоками.ВыгрузитьЗапрос_ТаблЗнач_GetRows(КомандаADODB.Execute()).ВыгрузитьКолонку(ИмяКлючевогоПоля);
КонецЕсли;
СтруктураМассивов = РазбитьМассивНаСтруктуруМассивов(л_МассивЗнач, ПарамФоновых.МаксЗначВЗапросе-1);
Если ЭтоЭмуляцияРаботы Тогда
ПарамФоновых.ВключеныПотокиФоновых = Истина;
Иначе
ПарамФоновых.ВключеныПотокиФоновых = СтруктураМассивов.Количество()-1 >= ПарамФоновых.ПорогДляПорожденияФоновых И НЕ ИнформационнаяБазаФайловая();
КонецЕсли;
инд = 0;
Для каждого КлючЗначение Из СтруктураМассивов Цикл
инд = инд + 1;
ТекстЗапроса = ТекстЗапросаДляАДС(КлючЗначение.Значение, ИмяКлючевогоПоля);
Если ПарамФоновых.ВключеныПотокиФоновых Тогда
Если инд%ПарамФоновых.МаксПотоковДержитСистема = 0 Тогда
ОтветыФоновых = СобратьОтветыФоновыхЗаданий(тз_Фоновых);
Если НЕ ОтветыФоновых.Успешно Тогда
Отказ = Истина;
Прервать;
КонецЕсли;
Если НЕ ЗначениеЗаполнено(л_ОбщаяТЗ) Тогда
л_ОбщаяТЗ = ОтветыФоновых.ОбщаяТЗ;
Иначе
ДобавитьСтрокиВТаблицу(л_ОбщаяТЗ, ОтветыФоновых.ОбщаяТЗ);
КонецЕсли;
КонецЕсли;
ЗапуститьДлительныеВычисленияНаСервере(ПараметрыПроцедуры(ТекстЗапроса), тз_Фоновых);
Иначе
КомандаADODB.CommandText = ТекстЗапроса;
Если НЕ ЭтоЭмуляцияРаботы Тогда
л_ВТтз = ИмпортПотоками.ВыгрузитьЗапрос_ТаблЗнач_GetRows(КомандаADODB.Execute());
КонецЕсли;
Если НЕ ЗначениеЗаполнено(л_ОбщаяТЗ) Тогда
л_ОбщаяТЗ = л_ВТтз.Скопировать();
Иначе
ДобавитьСтрокиВТаблицу(л_ОбщаяТЗ, л_ВТтз);
КонецЕсли;
КонецЕсли;
КонецЦикла;
Если ПарамФоновых.ВключеныПотокиФоновых И НЕ Отказ Тогда
ОтветыФоновых = СобратьОтветыФоновыхЗаданий(тз_Фоновых);
Если ОтветыФоновых.Успешно Тогда
Если НЕ ЗначениеЗаполнено(л_ОбщаяТЗ) Тогда
л_ОбщаяТЗ = ОтветыФоновых.ОбщаяТЗ;
Иначе
ДобавитьСтрокиВТаблицу(л_ОбщаяТЗ, ОтветыФоновых.ОбщаяТЗ);
КонецЕсли;
КонецЕсли;
КонецЕсли;
Возврат л_ОбщаяТЗ;
КонецФункции
Функция СобратьОтветыФоновыхЗаданий(Знач тз_Фоновых)
л_ОбщаяТЗ = Неопределено;
СтруктураОтвета = Новый Структура("Успешно,ОбщаяТЗ",Ложь,Неопределено);
массив_Фоновых = Новый Массив;
УспешноВернулось = 0;
Для каждого СтрокаФЗ Из тз_Фоновых Цикл
Если СтрокаФЗ.Отработано Тогда
Продолжить;
Иначе
массив_Фоновых.Добавить(СтрокаФЗ.ФЗ);
КонецЕсли;
КонецЦикла;
Если массив_Фоновых.Количество() = 0 Тогда
Возврат СтруктураОтвета;
КонецЕсли;
Попытка
ФоновыеЗадания.ОжидатьЗавершения(массив_Фоновых);
НачатьТранзакцию();
Для каждого СтрокаФЗ Из тз_Фоновых Цикл
Если СтрокаФЗ.Отработано Тогда
Продолжить;
Иначе
ЗаписьРС = РегистрыСведений.ВременныеИдентификаторыЗапросов.СоздатьМенеджерЗаписи();
ЗаписьРС.Идентификатор = СтрокаФЗ.Идентификатор;
ЗаписьРС.Прочитать();
Если ЗаписьРС.Выбран() Тогда
СтрокаФЗ.Отработано = Истина;
Если УспешноВернулось = 0 Тогда
л_ОбщаяТЗ = ЗаписьРС.Запрос.Получить();
Иначе
ДобавитьСтрокиВТаблицу(л_ОбщаяТЗ, ЗаписьРС.Запрос.Получить());
КонецЕсли;
УспешноВернулось = УспешноВернулось+1;
ЗаписьРС.Удалить();
КонецЕсли;
КонецЕсли;
КонецЦикла;
Если УспешноВернулось = массив_Фоновых.Количество() Тогда
СтруктураОтвета.Успешно = Истина;
СтруктураОтвета.ОбщаяТЗ = л_ОбщаяТЗ;
ЗафиксироватьТранзакцию();
КонецЕсли;
Исключение
ОтменитьТранзакцию();
ТекстОшибки = ОписаниеОшибки();
КонецПопытки;
Возврат СтруктураОтвета;
КонецФункции
Вызов фоновых заданий выполняется в цикле через метод ФоновыеЗадания.Выполнить(), который выполнит сделанную ранее в общем модуле экспортную процедуру. Фоновому заданию передается: ИмяОбщегоМодуля.ИмяЭкспортнойПроцедуры, массив параметров и идентификатор фонового задания, по которому потом будем собирать данные из РегистраСведений.
Метод ФоновыеЗадания.Выполнить()
ЗапуститьДлительныеВычисленияНаСервере
Процедура ЗапуститьДлительныеВычисленияНаСервере(Знач ПарамПроцедуры, тз_Фоновых)
СтрокаТЗ = тз_Фоновых.Добавить();
ЗаполнитьЗначенияСвойств(СтрокаТЗ, ПарамПроцедуры);
СтрокаТЗ.Отработано = Ложь;
ПараметрыЗадания = Новый Массив;
ПараметрыЗадания.Добавить(ПарамПроцедуры);
СтрокаТЗ.ФЗ = ФоновыеЗадания.Выполнить(ПарамПроцедуры.ИмяМетодаФЗ,ПараметрыЗадания,ПарамПроцедуры.Идентификатор);
КонецПроцедуры
На выходе получаем результирующие данные из параллельных фоновых заданий.
В заключение
Все примеры и листинги сделаны как эмуляция заполнения массивов и таблиц значений, а если прописать настоящий адрес, ключевое поле и имя таблицы и выключить эмуляцию, то работает с Advantage Database Server (далее – ADS), СУБД понимающей стандартный язык запросов T-SQL через Com объекты ADODB. Алгоритм можно использовать в любых похожих ситуациях с любыми СУБД, внешними службами, когда требуется ускорить/распарралелить вычисления.
В прилагаемом к статье файле конфигурация с 3 объектами для работы с ФоновымиЗаданиями 1С, включая все необходимые вспомогательные и служебные процедуры, функции, чтобы не зависеть от БСП и типа, версии конфигурации. Легко добавить в любую конфигурацию, если есть возможность изменения или как расширение и затем быстро подкорректировать под свою задачу. Формы Регистра и Обработки управляемые. Версия Платформы требуется не ниже 8.3.13, конфигурация любая.
Другие мои публикации на Инфостарте: