Как оптимально распределить документы для многопоточной обработки в 1С?

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

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

Решение 1: Деление общего количества на число заданий

Первый и самый очевидный подход — это арифметически разделить общее количество документов на желаемое количество потоков. Алгоритм заключается в том, чтобы вычислить базовое количество документов на одно задание и затем распределить остаток от деления по первым заданиям, добавляя им по одному документу.

Проанализируем пример кода, который реализует эту логику. Он вполне рабочий, но, как мы увидим далее, его можно сделать проще и элегантнее.

Пример кода:


Функция РаспределитьПТиУПоЗаданиям(п_Документы, п_КоличествоЗаданий) Экспорт

    Если ОбщегоНазначения.ИнформационнаяБазаФайловая Тогда
        л_КоличествоЗаданий = 1;
    Иначе
        л_КоличествоЗаданий = п_КоличествоЗаданий;
    КонецЕсли;

    л_Задания = Новый Соответствие;
    л_КоличествоДокументов = п_Документы.Количество();

    Если л_КоличествоЗаданий = 0 Или л_КоличествоДокументов = 0 Тогда
        Возврат л_Задания; // Возвращаем пустое соответствие, если нет заданий или документов
    КонецЕсли;

    л_ДокументыНаЗадание = Цел(л_КоличествоДокументов / л_КоличествоЗаданий);
    л_ОстатокДокументов = л_КоличествоДокументов % л_КоличествоЗаданий;
    л_НачальныйИндекс = 0;

    Для л_НомерЗадания = 1 По л_КоличествоЗаданий Цикл
        // Рассчитываем, сколько документов будет в текущем задании
        л_КоличествоДокументовВЗадании = л_ДокументыНаЗадание + ?(л_НомерЗадания <= л_ОстатокДокументов, 1, 0);
        
        л_МассивДокументов = Новый Массив;
        Для л_Индекс = л_НачальныйИндекс По л_НачальныйИндекс + л_КоличествоДокументовВЗадании - 1 Цикл
            л_МассивДокументов.Добавить(п_Документы[л_Индекс]);
        КонецЦикла;

        л_Задания.Вставить(л_НомерЗадания, л_МассивДокументов);
        л_НачальныйИндекс = л_НачальныйИндекс + л_КоличествоДокументовВЗадании;
    КонецЦикла;

    Возврат л_Задания;

КонецФункции

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

Решение 2: Циклическое распределение (Round-robin)

Более простой и часто используемый алгоритм — это циклическое распределение. Суть его в том, чтобы завести столько пустых массивов, сколько у нас планируется заданий, а затем в одном цикле пройти по всем документам и поочередно добавлять каждый документ в следующий массив "по кругу".

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

Важный момент по структуре данных: в исходном вопросе предлагалось использовать Соответствие, где ключ — это номер задания. Однако для набора заданий, которые по сути являются пронумерованными порциями данных, гораздо логичнее и удобнее использовать массив массивов. То есть, результатом будет массив, каждый элемент которого — это массив документов для одного конкретного задания.

Пример кода с циклическим распределением:


Функция РаспределитьДокументыПоЗаданиям(п_Документы, п_КоличествоЗаданий) Экспорт

    // Определяем реальное количество заданий. Нет смысла создавать больше заданий, чем документов.
    Если ОбщегоНазначения.ИнформационнаяБазаФайловая() Тогда
        л_КоличествоЗаданий = 1;
    Иначе
        л_КоличествоЗаданий = Мин(п_КоличествоЗаданий, п_Документы.Количество());
    КонецЕсли;
    
    Если л_КоличествоЗаданий = 0 Тогда
        Возврат Новый Массив;
    КонецЕсли;

    // 1. Инициализируем массив, который будет содержать массивы документов для каждого задания.
    л_Задания = Новый Массив;
    Для i = 1 По л_КоличествоЗаданий Цикл
        л_Задания.Добавить(Новый Массив);
    КонецЦикла;

    индекс_Задания = 0;
    // 2. В одном цикле проходим по всем документам
    Для Каждого Документ Из п_Документы Цикл
        // Добавляем текущий документ в массив текущего задания
        л_Задания[индекс_Задания].Добавить(Документ);
        
        // Переходим к следующему заданию "по кругу"
        индекс_Задания = индекс_Задания + 1;
        Если индекс_Задания = л_КоличествоЗаданий Тогда
            индекс_Задания = 0;
        КонецЕсли;
    КонецЦикла;

    Возврат л_Задания;

КонецФункции

Этот код более лаконичен и эффективен. Он не требует предварительных расчетов и сложных вложенных циклов, что делает его предпочтительным для простых задач распределения.

Решение 3: Продвинутый подход с учетом блокировок и зависимостей

Простые алгоритмы распределения, описанные выше, идеально работают для "сферической задачи в вакууме", когда все документы независимы друг от друга. В реальной работе это почти никогда не так. Если два параллельных потока попытаются провести документы, которые изменяют одни и те же регистры по одинаковым разрезам аналитики (например, списание одного и того же товара с одного склада), возникнет конфликт блокировок. В результате один из потоков будет ждать, а второй — выполняться, или, что хуже, оба могут завершиться с ошибкой блокировки. Весь выигрыш от распараллеливания будет потерян.

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

Шаг 1: Определение разделителей параллельной обработки

Необходимо выяснить, по каким ключевым полям (измерениям регистров) документы могут конфликтовать. Типичные разделители:

Задача — сгруппировать документы так, чтобы в одной группе оказались все документы, которые потенциально могут конфликтовать, а разные группы были независимы друг от друга.

Шаг 2: Группировка и распределение групп

После определения разделителей алгоритм меняется:

  1. Сгруппировать все документы по уникальным сочетаниям значений разделителей. Например, все документы по Складу №1 и Организации "Ромашка" попадают в одну группу.
  2. Распределить по заданиям не отдельные документы, а эти независимые группы. Если групп много, их можно распределять по потокам циклическим методом, как в Решении 2.
  3. Внутри каждого фонового задания документы из полученных групп должны обрабатываться строго последовательно.

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

Шаг 3: Динамическое распределение через общую очередь

Еще один надежный вариант, который хорошо адаптируется к документам разной "тяжести" (времени проведения), — это организация общей очереди задач.

  1. Создается временное хранилище (например, Регистр сведений) со списком всех документов для обработки. В регистре могут быть поля: ОбъектОбработки (ссылка на документ), НомерЗадания, СтатусОбработки.
  2. Запускается N фоновых заданий (потоков).
  3. Каждое фоновое задание в цикле выполняет следующие действия:
    • Устанавливает управляемую блокировку на регистр сведений, чтобы другие потоки не могли получить задачу одновременно.
    • Выбирает из регистра первый документ со статусом "К обработке" (или с пустым НомерЗадания).
    • Записывает в поле НомерЗадания свой уникальный идентификатор и меняет статус на "В обработке".
    • Снимает блокировку с регистра.
    • Проводит документ.
    • После успешного проведения меняет статус на "Обработан".
  4. Цикл повторяется, пока в регистре не останется необработанных документов. Если задание не находит документов для обработки, оно завершает свою работу.

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

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

← На главную