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