Многопоточная обработка данных в 1С простым способом

10.02.26

Разработка - Механизмы платформы 1С

Регламентные и фоновые задания в 1С обычно рассматриваются для выполнения задач, не требующих участия пользователя. Нередко они приводят к значительной нагрузке и потому выполняются в окна, когда основной поток пользователей не работают с базой. Что делать, если задание из-за возросших объемов данных перестало вписываться в рамки такого окна, а оптимизация недоступна? Как ни странно - использовать фоновые задания! Данная статья не открывает Америку, основана на работах предшественников, расширена, доведена до ума, расскажу, как этим пользоваться, ну и немного испытаем фоновые.

Оглавление:

1. Вступление

2. Краткий экскурс в работу конвейера

3. Примеры использования

4. Проверка скорости фоновых

5. Исходный код

 

1. Вступление

Итак, данные копятся, методы оптимизации экономят 10-15%, которые вскоре съедаются вновь возросшими аппетитами, мысли переходят к многопоточному выполнению, тем более данные не взаимосвязанные и могут обрабатываться вне зависимости друг от друга в произвольной последовательности. А может быть, вам просто нужно побыстрее обработать большой массив данных запроса, и ресурсы при прямом выполнении тратятся неэффективно. В таких случаях обычно обращаются к многопоточному выполнению, и здесь нам не обойтись без фоновых заданий.

Так как нам обработать массив данных быстрее? Конечно, разделить его выполнение между параллельными потоками. Для чего нам, собственно, понадобятся:

1. сам массив данных, который можно обрабатывать в любом порядке

2. процедура в общем модуле или модуле менеджера объекта, которая будет делать полезную работу с порцией этого массива. Как показывает практика, если данные можно обработать параллельно, то переработка функции в обработку порции занимает несколько минут.

3. фоновые задания для параллелизации. Конечно, ведь фоновые задания - это по сути новые сеансы, которые могут делать полезную работу.

4. немного времени. Да, к сожалению, операция разбиения между потоками фоновых заданий не бесплатная и каждый вызов нового потока занимает десятые доли секунды - у меня где-то 0,110 с. что на работе, что дома - завершение тоже не быстрое. Демонстрацию и проверку этого см. 4 раздел

5. конвейер, который будет контролировать как количество потоков, так и выгрузку данных в них и, если нужно, сбор результатов выполнения. Собственно, этот конвейер уже несколько раз был представлен в различных статьях (раз, два и три). От первой я и оттолкнулся, чтобы сделать более универсальный алгоритм, который можно будет многократно применять без доработки с любыми имеющимися входными данными, будь то массив или выборка.

 

2. Краткий экскурс в работу конвейера потоков

Проведу небольшой экскурс в особенности своего конвейера многопоточной обработки, он достаточно простой и уже не раз меня выручал в двух типах ситуаций: нужно обработать большой массив данных, медленный в обработке, и есть задача, результат которой зависит от случайности, хотелось бы получить несколько вариантов. Для первого служит процедура ВыполнитьФункциюПорциями, для второго - ВыполнитьНесколькоПотоковРасчета. Входными данными, как я уже сказал, может быть как простой массив (документов, например), так и массив структур, таблица значений или ВыборкаИзРезультатовЗапроса. На входе определяем структуру записи и потом заполняем ее для заполнения массива структур или простого массива для передачи в порцию.

Думаю, будет несложно переработать эту функцию для передачи в порцию Таблицы значений, но я остановился именно на массиве структур, потому что не знал о сериализуемости ТЗ, и он меня пока устраивает для моих задач. P.S. Сделано

Когда данные собраны, можно запускать поток, если он, конечно, не лишний:

// запуск фонового задания и его фиксация
МассивПараметров = Новый Массив; 
МассивПараметров.Добавить(ДанныеДляЗадания);
Если НастройкиВыполнения.ВозвращатьРезультаты Тогда
    Адрес = ПоместитьВоВременноеХранилище(Неопределено, Новый УникальныйИдентификатор);
    МассивПараметров.Добавить(Адрес);
КонецЕсли;
Для Каждого Запись Из МассивПоследующихПараметров Цикл
    МассивПараметров.Добавить(Запись);
КонецЦикла;
Если НастройкиВыполнения.ВыполнятьБезопасноИлиИзМенеджера Тогда
    ПараметрыЗадания = Новый Массив;
    ПараметрыЗадания.Добавить(ВыполняемыйМетод);
    ПараметрыЗадания.Добавить(МассивПараметров);

    ПараметрыЗадания.Добавить(Неопределено);
    ФЗ = ФоновыеЗадания.Выполнить("ДлительныеОперацииСервер.ВыполнитьБезопасно", ПараметрыЗадания,, КлючЗадания);
Иначе
    ФЗ = ФоновыеЗадания.Выполнить(ВыполняемыйМетод, МассивПараметров, , КлючЗадания);
КонецЕсли;
АктивныеЗадания.Добавить(Новый Структура("ИдентификаторФЗ, АдресХранилища", ФЗ.УникальныйИдентификатор, Адрес));
            

Как видно, первым параметром для функции обработки порции должна быть собственно порция, вторым в случае необходимости возвращения результата адрес хранилища, в который результат и помещается, далее остальные параметры, которые для каждого потока будут одинаковыми. Разумеется, если результат не нужен, резервировать второй параметр под адрес хранилища не нужно. Как видим, сам поток организуется очень просто: это вызов функции менеджера фоновых заданий Выполнить.

Далее нужно отследить выполнение потока, для чего используется функция ОбработкаОчередиЗаданий, вот основной цикл:

ОбнаруженаАвария = Ложь;
// проверка наличия свободных потоков
Индекс = АктивныеЗадания.Количество();
Пока Индекс > 0 Цикл
            
    Индекс = Индекс - 1;
    // смысл в том, чтобы проверить статус у запущенного задания, и если он не активен - освободить место в массиве ФЗ
    ТекущееЗадание = ФоновыеЗадания.НайтиПоУникальномуИдентификатору(АктивныеЗадания[Индекс].ИдентификаторФЗ);
    Если ТекущееЗадание.Состояние = СостояниеФоновогоЗадания.Завершено Тогда
        Если НастройкиВыполнения.ВозвращатьРезультаты Тогда
            ПополнитьРезультаты(АктивныеЗадания[Индекс].АдресХранилища, НастройкиВыполнения.ОбъединятьРезультаты, 
                РезультатыВыполнения);
        КонецЕсли;
    ИначеЕсли ТекущееЗадание.Состояние <> СостояниеФоновогоЗадания.Активно Тогда
        ОбнаруженаАвария = Истина;
        ОчередьБезаварийная = Ложь;
        Если ТекущееЗадание.Состояние = СостояниеФоновогоЗадания.Отменено Тогда
            ЗаписьЖурналаРегистрации(НастройкиВыполнения.ПотокЖурналаРегистрации, УровеньЖурналаРегистрации.Ошибка, , , 
                "Одна из порций задания с вызовом метода """ + ТекущееЗадание.ИмяМетода + 
                """ была отменена. Выполнение будет остановлено");
            НастройкиВыполнения.ОстанавливатьПриАвариях = Истина;
        КонецЕсли;
    Иначе
        Продолжить;
    КонецЕсли;
    АктивныеЗадания.Удалить(Индекс);
    Для Каждого Сообщение Из ТекущееЗадание.ПолучитьСообщенияПользователю(Истина) Цикл
        Сообщение.Сообщить();
    КонецЦикла;

КонецЦикла;

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

Насчет пополнения результатов - функция нифига не универсальная, возвращает либо массив результатов каждой обработки порции, который потом самому разгребать, либо если результатом является тот же массив (структур), то результаты можно объединить в общий массив. Не нравится - переделывайте!

Для подведения итогов запуска используется функция ФоновыеЗадания.ПолучитьФоновыеЗадания с ключом по Наименованию. Поскольку все задания в потоке запускались с уникальным названием (ключ UID), их легко отследить и обработать результаты.

 

3. Примеры использования

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

Текст = Обработки.ДемонстрацияФоновыхЗаданий.ПолучитьМакет("ВойнаИМирДляПроверкиУведомлений").ПолучитьТекст();
НаборСлов = ДлительныеОперацииСервер.РазделитьСтроку(Текст, " ,.-""«»[{@#}]'…();:!?" + Символы.ПС
	+ Символы.НПП + Символы.ВК + Символы.Таб);
	
НастройкиВыполнения = ДлительныеОперацииСервер.НастройкиВыполнения(15, , , Ложь);
ДлительныеОперацииСервер.ВыполнитьФункциюПорциями(НаборСлов, 
	"Обработки.ДемонстрацияМногопоточнойРаботы.НагрузитьСправочникТоварыПоПорциям",
	Новый Массив, НастройкиВыполнения);
	
ОбщегоНазначения.СообщитьПользователю("Готово! Справочник ""Товары"" загружен в количестве " 
	+ НаборСлов.Количество() + " элементов.");

Первые две строчки мы собираем данные, вторыми запускаем конвейер, пятой строчкой сообщаем о результатах. Как видим, все очень и очень просто (было бы также с новыми функциями БСП для многопоточной работы, этой статьи бы не было). Не буду приводить здесь функцию обработки порции, она слишком простая (ищем в справочнике по слову в цикле, если такого слова еще нет, создаем элемент справочника). Если вам интересно, сколько уникальных слов в первых двух томах, то я вас не порадую, ибо данный алгоритм абсолютно не учитывает словоформы, да и разделители не все найдены точно. Так что их набралось более 30 тысяч, да.

Разумеется, нужно рассказать и о методе НастройкиВыполнения. В эти самые настройки входят:
1. КоличествоПотоков - количество одновременно запускаемых потоков
2. ВыполнятьБезопасноИлиИзМенеджера - в старом БСП есть метод ВыполнитьБезопасно, который тем не менее не использует безопасный режим, но предоставляет способ выполнения из менеджера объекта - используем его или прямое выполнение (думал, будет разница в производительности - нет ее)
3. ОбъединятьРезультаты - если результаты - это массив, то можно воспользоваться объединением результатов в один массив
4. СтрокВПорции - можно не задавать, тогда объем данных будет поделен между потоками, а так размер одной порции
5. ОстанавливатьПриАвариях - если при любой критической ошибке в любом потоке нужно останавливать выполнение конвейера, ставим Истину
6. ВозвращатьРезультаты - будет это процедура или процедура, которая помещает результат по  адресу, переданному вторым параметром функции обработки порции
7. ПотокЖурналаРегистрации - я беру на себя смелость писать некоторые факты в журнал регистрации, это собственно ИмяСобытия
8. ПередаватьТаблицуЗначений - что передавать в задание - таблицу значений или массив (структур, например)

Первый пример был простым, поскольку не требовал возвращения результата, давайте испытаем что-нибудь, что все таки требует отчета, например, подчистим наш справочник от неиспользуемых товаров и вернем используемые:

Запрос = Новый Запрос;
Запрос.Текст =
	"ВЫБРАТЬ
	|	Товары.Ссылка КАК Ссылка
	|ИЗ
	|	Справочник.Товары КАК Товары";
РезультатЗапроса = Запрос.Выполнить();
ВыборкаДетальныеЗаписи = РезультатЗапроса.Выбрать();
	
НастройкиВыполнения = ДлительныеОперацииСервер.НастройкиВыполнения(15);
НеудаляемаяНоменклатура = ДлительныеОперацииСервер.ВыполнитьФункциюПорциями(ВыборкаДетальныеЗаписи
	, "Обработки.ДемонстрацияМногопоточнойРаботы.ПодчиститьСправочникТоварыПоПорциям", 
	Новый Массив, НастройкиВыполнения);
	
ОбщегоНазначения.СообщитьПользователю("Готово! Справочник ""Товары"" очищен от неиспользуемой номенклатуры");
ОбщегоНазначения.СообщитьПользователю("Неудаляемая номенклатура: ");
Для Каждого Товар Из НеудаляемаяНоменклатура Цикл
	ОбщегоНазначения.СообщитьПользователю(Символы.Таб + Товар.Наименование);
КонецЦикла;

Нда, сложнее не получилось, наоборот, в функции настроек не нужно указывать четвертый параметр (возвращать ли результаты - по умолчанию возвращать). Зато функция возвращает нам простой массив номенклатуры, которую нельзя удалять, о каждой из которых мы и сообщаем. Как видим, мы запускаем 15 потоков. На моем домашнем ПК 18 потоков (ядер 14) - часть потоков нужна ОС и базе данных, поэтому выбор такой. Поскольку мы не указываем размер порции (второй параметр), то выборка будет распределена между 15 потоками, которые все сразу и запустятся. Можно сделать количество потоков меньшим, но указать размер порции (не забывайте, что вызов фонового задания не бесплатный, да и в фоновое задание нельзя передавать больше 1ГБ данных (это сотни тысяч строк, если не миллионы, в зависимости от количества и наполнения колонок)). Транзакциями занимайтесь в функции обработки порции, если хотите - фоновые задания сами по себе транзакцией быть не могут, ха-ха.
Кстати, приведу ради примера функцию, которую я использовал для очистки товаров порциями, чтобы вы не думали, что она чем-то усложнена:

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

ПоместитьВоВременноеХранилище(НеудаляемыеСсылки, АдресРезультата);

Ну и третьим примером покажу использование многократного повторения процедуры для одних и тех же данных (ВыполнитьНесколькоПотоковРасчета). Есть такое понятие как "каламбургер", то есть слово, которое состоит из двух других пересекающихся слов. Представим, что нам быстро нужно найти большое количество таких каламбургеров для демонстрации (по требованию пользователя - от 50 до 750). Поскольку алгоритм крайне простой, он скорее всего не будет вами распараллеливаться, но как мы убедимся, даже на достаточно небольшом размере требуемых данных многопоточное исполнение победит однопоточное, несмотря на издержки:

Запрос = Новый Запрос;
Запрос.Текст =
    "ВЫБРАТЬ
    |    Товары.Наименование КАК Наименование
    |ИЗ
    |    Справочник.Товары КАК Товары";
РезультатЗапроса = Запрос.Выполнить();
СписокСлов = РезультатЗапроса.Выгрузить();

Отметка = ТекущаяУниверсальнаяДатаВМиллисекундах();
НаборыКаламбургеров = ДлительныеОперацииСервер.ВыполнитьНесколькоПотоковРасчета(СписокСлов, 
    "Обработки.ДемонстрацияМногопоточнойРаботы.НайтиКаламбургеры",
    ОбщегоНазначенияКлиентСервер.ЗначениеВМассиве(Цел(КоличествоКаламбургеров / 5)), 10, Истина, Ложь);
Каламбургеры.Очистить();
Для Каждого Набор Из НаборыКаламбургеров Цикл
    Для Каждого Запись Из Набор Цикл
        СтруктураПоиска = Новый Структура("Каламбургер", Запись.Каламбургер);
        Если Каламбургеры.НайтиСтроки(СтруктураПоиска).Количество() = 0 Тогда
            ЗаполнитьЗначенияСвойств(Каламбургеры.Добавить(), Запись);
        КонецЕсли;
        Если Каламбургеры.Количество() = КоличествоКаламбургеров Тогда
            Прервать;
        КонецЕсли;
    КонецЦикла;
Если Каламбургеры.Количество() = КоличествоКаламбургеров Тогда
        Прервать;
    КонецЕсли;
КонецЦикла;
ОбщегоНазначения.СообщитьПользователю("Многопоточным алгоритмом за "
     + (ТекущаяУниверсальнаяДатаВМиллисекундах() - Отметка) + " мс найдено "
     + Каламбургеры.Количество() + " каламбургеров");

Отметка = ТекущаяУниверсальнаяДатаВМиллисекундах();
Каламбургеры.Загрузить(Обработки.ДемонстрацияМногопоточнойРаботы.НайтиКаламбургеры(
    СписокСлов, , КоличествоКаламбургеров));
ОбщегоНазначения.СообщитьПользователю("Сплошным алгоритмом за "
     + (ТекущаяУниверсальнаяДатаВМиллисекундах() - Отметка) + " мс найдено "
     + Каламбургеры.Количество() + " каламбургеров");

На моем ПК многопоточные начинают побеждать на 420-450 штук каламбургеров, но алгоритм хорошо оптимизирован, а основная нагрузка идет на составлении быстрого потом соответствия.

Здесь вызов немного сложнее. Кроме таблицы со словами, которая является основным набором для функции и собственно самой функции передается массив последующих параметров (здесь это только число каламбургеров), количество потоков (10), выполнение из менеджера объектов (стоит Истина) и объединять или нет результаты. Поскольку результатом одного потока будет таблица значений, объединять результаты мы здесь не будем (стоит Ложь). Отдельной функции для настроек я делать не стал, поскольку параметров у функции итак немного. Потом мы обрабатываем полученные с запасом таблицы каламбургеров, вставляя уникальные в таблицу на форме.

 

 

Как видно по результатам, пресловутые накладные расходы держат результат многопоточного выполнения около секунды независимо от объема работы, а в вот прямой подход показывает превосходство только на малых объемах.

 

4. Проверка скорости фоновых заданий

В какой-то момент стало интересно, почему, несмотря на все мои ухищрения, добиться многопоточного поиска быстрее 700 мс практически невозможно, поэтому я добавил в обработку команду "Проверка скорости фоновых", которая делает 5 вещей:

1. запускает (количество) фоновых заданий, которые ничего не делают, сразу завершаются - для проверки скорости чистого механизма отработки фоновых. Завершение заданий проверяется в бесконечном цикле без паузы

2. собирает миллион в массив такого же размера - для последующего сравнения продолжительности с фоновыми

3. раздает задание собрать миллион между (количеством) потоков - общее время также замеряется для сравнения с пустыми заданиями и линейными алгоритмом

4. передает собранный миллион (массив) в (количество) потоков и добирает там миллион теми же долями, что и в 3) - для проверки времени, которое тратится на передачу данных в фоновое задание

5. передает массив в (количество) потоков, а собирает там новый. Дело в том, что сравнивая результаты замеров по фоновым заданиям и линейному алгоритму, заподозрил, что с входными данными задание работает медленнее, чем с внутренними, поэтому сравниваю с 4)

1 пустых фоновых заданий (параллельно) выполнилось за 276 мс.
Ты собрал миллион за 2 353 мс.
1 фоновых заданий (параллельно) собрали миллион за 2 699 мс.
1 фоновых заданий (параллельно) удвоили миллион за 3 083 мс.
1 фоновых заданий (параллельно) собрали свой миллион, глядя на ваш, за 3 156 мс.


5 пустых фоновых заданий (параллельно) выполнилось за 305 мс.
Ты собрал миллион за 2 388 мс.
5 фоновых заданий (параллельно) собрали миллион за 1 056 мс.
5 фоновых заданий (параллельно) удвоили миллион за 1 918 мс.
5 фоновых заданий (параллельно) собрали свой миллион, глядя на ваш, за 1 948 мс.


10 пустых фоновых заданий (параллельно) выполнилось за 290 мс.
Ты собрал миллион за 2 325 мс.
10 фоновых заданий (параллельно) собрали миллион за 772 мс.
10 фоновых заданий (параллельно) удвоили миллион за 2 460 мс.
10 фоновых заданий (параллельно) собрали свой миллион, глядя на ваш, за 2 431 мс.

То есть видно, что удвоение числа потоков не приводит к удвоению скорости в самом простом случае (хотя ядер, как я говорил, у меня с запасом), а при большом объеме входных данных даже замедляет процесс, поскольку накладные расходы уже превышают внутренние. Ну и нужно понимать, что после сбора в фоновых еще нужно бы собрать массив воедино, но мы здесь этого не делаем. Не забываем и о том, что не все алгоритмы такие простые, как этот, а то еще загрустите! На некоторых можно добиться ускорения и на порядок, что будет очень сильно заметно. Кстати, многопоточное заполнение справочника из примера быстрее однопоточного более чем в 6 раз (230 секунд против 37.8)!

 

5. Исходный код

Привожу здесь исходный код всего модуля ДлительныеОперацииСервер, может, кому пригодится:

 
  Исходный код ДлительныеОперацииСервер

Также можно скачать проект для EDT на сайте gitflic по ключевым словам "фоновые задания". Там кроме БСП-шных будет две обработки, одна по статье Фоновые задания для новичков, другая по этой - Демонстрация многопоточной работы. Кстати, БСП для внедрения необязательна. Приятной многопоточной работы всем!

Вступайте в нашу телеграмм-группу Инфостарт

многопоточная обработка фоновые задания

См. также

Механизмы платформы 1С Программист Бесплатно (free)

Разберем 15 мифов о работе платформы «1С:Предприятие 8» – как распространенных, так и малоизвестных. Начнем с классики: «Код, написанный в одну строку, работает быстрее, чем многострочный». Так ли это на самом деле?

16.07.2025    28041    TitanLuchs    106    

147

Механизмы платформы 1С Работа с интерфейсом Программист Стажер 1С:Предприятие 8 Бесплатно (free)

Про ООП в 1С и о том, как сделать свой код более кратким и выразительным при помощи использования текучего интерфейса (fluent interface).

03.02.2025    15371    bayselonarrend    127    

68

Механизмы платформы 1С Программист 1С:Предприятие 8 Бесплатно (free)

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

30.01.2025    17663    user2122906    9    

62

Механизмы платформы 1С WEB-интеграция Программист 1С:Предприятие 8 Бесплатно (free)

В платформе 8.3.27 появилась возможность использовать WebSocket-клиент. Давайте посмотрим, как это все устроено и чем оно нам полезно.

14.01.2025    28317    dsdred    89    

143

Механизмы платформы 1С Программист Стажер 1С:Предприятие 8 1C:Бухгалтерия Бесплатно (free)

Эта небольшая статья - некоторого рода шпаргалка по файловым потокам: как и зачем с ними работать, какие преимущества это дает.

23.06.2024    25720    bayselonarrend    22    

175

Механизмы платформы 1С Программист Стажер 1С:Предприятие 8 1C:Бухгалтерия Бесплатно (free)

Пример использования «Сервисов интеграции» без подключения к Шине и без обменов.

13.03.2024    13994    dsdred    22    

85
Комментарии
Подписаться на ответы Инфостарт бот Сортировка: Древо развёрнутое
Свернуть все
1. user-z99999 77 10.02.26 11:09 Сейчас в теме
А почему вы разбиваете работу на порции, никак не вмешиваясь в логику порций?
Иногда на нижнем уровне нужно сразу что-то пересчитать для всех,
а потом на верхних уровнях уже делить на порции. Это как пример.
Тупа разбивая на порции, можно получить блокировки ожидания т.к. все потоки делают одинаковую работу.
Да, вы блокировки можете получить не на уровне 1с, а на уровне базы данных.

Многопоточность зависит от структуры базы и запросов к ней, и от индексов и от плана запроса и т.д.
2. n_mezentsev 80 10.02.26 11:27 Сейчас в теме
(1) Подразумевается, что порции несвязанные, а все необходимые данные уже есть. По поводу блокировок. Сама запись обычно процедура очень быстрая, как-то в один миллионный регистр писал до 30 потоков, никаких проблем не наблюдал, да и в проблемной базе, где запись в тот же регистр шла до 1.5-2 секунд блокировки ловил крайне редко - возможно, потому что писал рекомендуемым МенеджеромЗаписи, не знаю. В примере в демобазе пишется справочник в 15 потоков - никаких проблем, при том что параллельно еще идут операции поиска по справочнику. Проблемы блокировки начинаются когда? Когда транзакция записи уже началась, а алгоритм тормозит выполнение на неопределенное время, поэтому в ERP сперва заполняются таблицы движений, потом идет запись - здесь тот же принцип сработает также.
3. gvorhin 5 10.02.26 14:58 Сейчас в теме
(2) Я бы тут, кажется, развёл два разных слоя проблемы.

Разбиение на порции — это всё-таки про управление внешним параллелизмом, а не про корректность алгоритма. Если на нижнем уровне действительно есть общая фаза пересчёта или подготовки данных, то она, по сути, уже не параллелизуемая часть и должна быть вынесена отдельно — до конвейера. Иначе мы действительно получаем «ложную многопоточность», где все потоки делают одно и то же.
А вот дальше начинается самое интересное: даже при формально несвязанных порциях эффект упирается не в 1С, а в БД — планы запросов, горячие индексы, конкуренцию за одни и те же страницы. И тут уже не всегда важно, пишем ли мы через МенеджерЗаписи или нет — можно получить деградацию без явных блокировок, просто за счёт роста ожиданий.
Поэтому вопрос, который у меня обычно возникает в таких схемах: а как вы для себя определяете, что порции действительно независимы с точки зрения БД, а не только бизнес-логики? По ощущениям, именно здесь чаще всего и проходит граница между «ускорили в разы» и «ускорили одну операцию, замедлив систему».
n_mezentsev; +1 Ответить
4. booksfill 10.02.26 16:32 Сейчас в теме
(3)
можно получить деградацию без явных блокировок, просто за счёт роста ожиданий


Конечно можно получить рост PAGELATCH_EX, вплоть до ошибки ожидания на блокировках.
И да - бить на порции лучше все же согласно с тем положением, что " таблицы хранятся физически по ключу кластеризованного индекса".

Но, учитывая неторопливость1С при работе с СУБД, вероятность этого мала.

P.S.
Например в ТЗ можно сортировать по колонке со случайными данными.
Тут уже все зависит от конкретной задачи.
Принцип (и, увы, цена вопроса) тот же что и в чем-то типа
SEL ECT Period, Ref1, Ref2 fr om [dbo].[_InfoRg25109] 
order by NEWID()
7. gvorhin 5 10.02.26 17:25 Сейчас в теме
(4) Согласен, если смотреть на это именно как на работу с физическим порядком данных, то разбиение по ключу кластеризованного индекса — практически единственный осмысленный вариант для масштабируемого параллелизма. В этом смысле порции — это уже не просто «кусочки массива», а попытка минимизировать конкуренцию за страницы.
Но мне тут кажется важным одно уточнение. Даже если 1С сама по себе «нетороплива», она всё равно умеет достаточно эффективно создавать конкуренцию на уровне СУБД — просто за счёт количества сеансов. И рост PAGELATCH_EX часто прилетает не из-за скорости записи, а из-за того, что несколько потоков начинают регулярно встречаться на одних и тех же горячих участках индекса.
Именно поэтому сортировка по случайному признаку (или условный NEWID()) — палка о двух концах: да, мы размазываем обращения по страницам, но при этом теряем предсказуемость доступа и часто платим за это планом запроса и лишними чтениями. В одних задачах это спасает, в других — наоборот, добивает.
По ощущениям, самый сложный момент здесь даже не в выборе стратегии разбиения, а в том, что граница «ещё ускоряем» → «уже деградируем» почти всегда определяется экспериментально и сильно зависит от конкретной схемы индексов и нагрузки базы. Универсального рецепта, увы, так и не появилось.
8. booksfill 10.02.26 18:04 Сейчас в теме
(7)
и часто платим за это планом запроса и лишними чтениями

Именно так, убираем проблемы с записью, получаем избыточную нагрузку при SELECT, который 1С делает перед записью.

Полностью согласен " Универсального рецепта, увы, так и не появилось".

А распараллеливание чисто вычислительных задач, не работающих с СУБД, с разбивкой на потоки...
Даже и не знаю, тогда лучше уж смотреть в строну микросервисов на предназначенных для такой работы языках.
Да, теряем время на передачу (например через брокер) данных, но скорость обработки информации будет на порядки быстрей, да и пожирания серверных лицензий не будет (вроде как фоновое ест серверные) + множество нужных библиотек под рукой.
9. gvorhin 5 10.02.26 18:48 Сейчас в теме
(8) Да, вот тут, по-моему, и проходит ключевая граница применимости. Как только в схеме появляется обязательный SELECT перед записью (а в 1С он почти всегда есть), мы просто переносим нагрузку: снимаем конкуренцию на INSERT/UPDATE и получаем её на чтении, часто ещё и с менее предсказуемым планом.
С вычислительными задачами без СУБД согласен почти полностью. В таких случаях фоновые задания в 1С — это скорее «вынужденный компромисс», чем оптимальный инструмент. Они удобны, когда нужно остаться внутри платформы и данных, но как только задача становится реально CPU-bound, стоимость сеанса и сериализаций начинает доминировать.
Поэтому на практике у меня обычно получается так:
если задача тесно связана с данными 1С и транзакционной моделью — играем конвейером, порциями и аккуратным контролем конкуренции, если это чистая математика / перебор / анализ — гораздо логичнее выносить наружу, даже с учётом брокеров и задержек на передачу. Потери на интеграции окупаются предсказуемостью и масштабируемостью.
В этом смысле статья как раз хороша тем, что показывает цену фоновых заданий, а не продаёт их как универсальное решение.
11. n_mezentsev 80 10.02.26 19:21 Сейчас в теме
(8)
вроде как фоновое ест серверные
не трогает фоновое задание лицензии! Проверял на 30 тысячах фоновых на комьюнити лицензии)
5. n_mezentsev 80 10.02.26 16:45 Сейчас в теме
(3) Если беспокоит именно запись, ничего не мешает вернуть готовые к записи данные и уже в однопоточном режиме записать их, возможно, уменьшая "взаимные" блокировки, но не уменьшая общую нагрузку на систему (будет записан тот же объем информации). Но все-таки согласитесь, что обычно эффект от многопоточности будет достигнут не "за счет" высококонкурентной записи, а за счет разлива нагрузки вычислительного алгоритма между потоками и общая отзывчивость системы в этот момент может страдать (у меня в примерах заняты на 100% 16 ядер из 18), но нужно понимать, что это сконцентрированная нагрузка, которая длится в разы меньше чем однопоточная, но на частых операциях злоупотреблять я бы все равно не стал
6. gvorhin 5 10.02.26 17:11 Сейчас в теме
(5) Да, здесь полностью согласен. Если вынести запись в однопоточный этап, то мы по сути просто меняем профиль нагрузки, а не её объём — выигрываем в управляемости блокировок, но не в «дешевизне» операции.
Мне кажется, ключевая мысль как раз в том, что многопоточность в таких сценариях почти всегда покупается за счёт ухудшения отзывчивости системы здесь и сейчас. И тогда вопрос уже не «быстрее или медленнее», а «какой профиль деградации мы считаем допустимым». Концентрированная нагрузка на CPU на коротком интервале зачастую оказывается менее болезненной, чем растянутая однопоточная, но это работает ровно до тех пор, пока задача не становится регулярной.
В этом смысле очень откликается мысль «не злоупотреблять на частых операциях». По ощущениям, такие конвейеры хорошо ложатся на редкие, но тяжёлые расчёты, а вот вблизи OLTP-нагрузки граница применимости наступает очень быстро — даже без явных блокировок и ошибок.
Интересно, рассматривали ли вы в подобных схемах динамическое ограничение параллелизма (например, в зависимости от времени суток или текущей загрузки), или считаете, что это уже избыточное усложнение?
10. n_mezentsev 80 10.02.26 19:18 Сейчас в теме
(6) Интересный вопрос. Но он у меня не возникал, наверное потому, что разрабатывал это для обычных форм и толстого клиента, где сервер не воспринимается как что-то вечно загруженное работой. А так, на ум приходит короткий, но по возможности синтетический тест пускай в однопотоке с хранением результатов в общем хранилище для определения того, насколько можно дозагрузить систему. Думаю, было бы интересно, сможет ли какой-либо тест определить, что сейчас идет операция закрытия месяца)
12. gvorhin 5 10.02.26 19:33 Сейчас в теме
(10) Да, понимаю этот контекст — когда сервер воспринимается как «ресурс под конкретную операцию», а не как постоянно нагруженная платформа. В таких условиях действительно проще мыслить статическими лимитами и не усложнять механику.
Мне, правда, кажется, что как только 1С начинает жить в режиме смешанной нагрузки (OLTP + регламентные + фоновые), вопрос динамики всё равно всплывает, даже если изначально его не закладывали. Причём не столько в виде «умного адаптивного алгоритма», сколько в виде очень грубых правил: время суток, тип операции, признак «регламентная/пользовательская».
Идея синтетического теста откликается, но, по ощущениям, он скорее хорошо ловит средний профиль нагрузки, чем пики. Закрытие месяца или массовые перепроведения как раз тем и коварны, что деградация там начинается не с CPU, а с очередей ожиданий, и тест может просто не успеть это отразить.
В итоге у меня чаще всего всё сводилось к довольно приземлённому компромиссу: фиксированные лимиты плюс организационные правила запуска тяжёлых конвейеров. Не потому что это красиво, а потому что в реальной эксплуатации предсказуемость часто оказывается важнее адаптивности.
Интересно, кстати, где у вас на практике проходила грань, после которой сервер начинал «чувствоваться» пользователями — по CPU, по времени ответа, по количеству активных сеансов?
Для отправки сообщения требуется регистрация/авторизация