Многопоточность в БСП реализуется функциями общего модуля ДлительныеОперации: ВыполнитьФункциюВНесколькоПотоков и ВыполнитьПроцедуруВНесколькоПотоков. Вы передаёте имя экспортного серверного метода, параметры выполнения и соответствие «ключ порции → массив параметров метода». БСП запускает фоновые задания по порциям, следит за ними и после завершения кладёт сводный результат во временное хранилище.
Функцию выбирают, когда результат порций нужно собрать и обработать дальше; процедуру — когда достаточно выполнить работу и узнать, что всё прошло без ошибок. Ниже — схема, рабочий код для запуска с клиента и из регламентного задания, и ошибки, которые встречаются чаще всего. Пример ориентирован на БСП 3.1; в более ранних версиях сигнатуры отличаются, поэтому перед использованием открывайте комментарии функций в своей конфигурации.
Параметры функций многопоточности
| Параметр | Что передаём |
|---|---|
ИмяФункции / ИмяПроцедуры | Строка "ОбщийМодуль.Метод", метод экспортный, серверный |
ПараметрыВыполнения | Структура из ДлительныеОперации.ПараметрыВыполненияФункции(ИдентификаторФормы) или ПараметрыВыполненияПроцедуры() |
НаборПараметров | Соответствие: ключ — идентификатор порции (номер, ссылка), значение — Массив параметров метода |
Количество параметров метода в массиве ограничено (по описанию функции — не больше семи). На практике хватает одного: порции данных.
Обе функции возвращают структуру с описанием фонового задания — ту же, что ДлительныеОперации.ВыполнитьФункцию: статус, идентификатор задания, адрес результата. После завершения по адресу результата лежит соответствие с теми же ключами, что в наборе параметров, а в значениях — описание результата каждой порции, включая её статус и адрес её собственного результата.
Как спроектировать многопоточную обработку
Перед кодом решите три вопроса:
- Нужен ли результат порций? Да — функция; нет — процедура.
- Как резать данные на порции? Порции должны быть независимыми: один документ — в одном потоке, одна запись регистра не должна писаться из двух потоков.
- Сколько этапов? Если сначала идёт расчёт, а потом запись, разделите их. Расчёт параллелится свободно, запись — по документам.
Минимальный набор методов:
- серверный метод, который готовит порции и запускает потоки;
- серверный экспортный метод обработки одной порции;
- при запуске с клиента — клиентская процедура с ожиданием и обработчик завершения;
- при необходимости — метод постобработки собранных результатов.

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

Почему два этапа, а не один? Если в одном потоке и считать, и писать, а порции резать по строкам данных, два потока могут попасть в один документ: получите либо неверное заполнение, либо ожидание на блокировке. Расчёт без записи не блокирует ничего, а запись по документам не пересекается.
Запуск из регламентного задания
На сервере ждать завершения можно прямо в вызове: значение Неопределено в ОжидатьЗавершение означает «ждать, пока задание не закончится».
// Обработчик регламентного задания.
Процедура РасчетДокументовРегламент() Экспорт
ОбщегоНазначения.ПриНачалеВыполненияРегламентногоЗадания(
Метаданные.РегламентныеЗадания.РасчетДокументов);
Период = Новый СтандартныйПериод(НачалоМесяца(ТекущаяДатаСеанса()), КонецМесяца(ТекущаяДатаСеанса()));
НаборПараметров = НаборПараметровПоПорциям(ПодготовитьДанныеДляРасчета(Период), 8);
ПараметрыВыполнения = ДлительныеОперации.ПараметрыВыполненияФункции(Новый УникальныйИдентификатор);
ПараметрыВыполнения.НаименованиеФоновогоЗадания = НСтр("ru = 'Расчет документов (регламент)'");
ПараметрыВыполнения.ОжидатьЗавершение = Неопределено;
Результат = ДлительныеОперации.ВыполнитьФункциюВНесколькоПотоков(
"РасчетДокументовСервер.РассчитатьПорцию", ПараметрыВыполнения, НаборПараметров);
Если Результат.Статус <> "Выполнено" Тогда
ЗаписьЖурналаРегистрации(НСтр("ru = 'Расчет документов'"), УровеньЖурналаРегистрации.Ошибка, , ,
Результат.ПодробноеПредставлениеОшибки);
Возврат;
КонецЕсли;
// Далее - сбор результатов и запись, как в этапе 2
КонецПроцедурыВозможные статусы: "Выполняется", "Выполнено", "Ошибка", "Отменено".
Сколько потоков и какого размера порции
Универсального числа нет, но есть порядок подбора, которым я пользуюсь:
- Замер на одном потоке. Запустите обработку с
ЧислоПотоков = 1на копии базы и запишите время. Это базовая линия. - Удвоение. Пробуйте 2, 4, 8 потоков. Пока время падает почти пропорционально — узкого места нет.
- Точка насыщения. Когда рост числа потоков перестаёт сокращать время, смотрите, во что упёрлись: процессор рабочих процессов сервера 1С, диск или процессор СУБД, ожидания на управляемых блокировках.
- Запас для пользователей. Если обработка идёт днём, оставьте ресурсы для обычной работы: лучше 4 потока и спокойные пользователи, чем 12 потоков и жалобы.
Размер порции тоже важен. Слишком мелкие порции — много накладных расходов на запуск фоновых заданий и передачу параметров. Слишком крупные — один «тяжёлый» поток заканчивает позже всех, и общее время определяется им. Хорошее правило — порций столько же, сколько потоков, а если объекты сильно различаются по трудоёмкости, порций в 2–3 раза больше, чтобы нагрузка выровнялась.
Отладка многопоточного кода
Фоновые задания отлаживать неудобно, поэтому я строю код так, чтобы его можно было проверить без потоков:
- метод обработки порции (
РассчитатьПорцию) вызывается и напрямую, с маленьким массивом, — так его можно пройти отладчиком; - в БСП есть параметр выполнения, позволяющий запустить длительную операцию не в фоне (в версиях 3.1 —
ЗапуститьНеВФоне), — удобно для отладки; - ошибки порций пишутся в журнал регистрации с ключом порции, чтобы по журналу было видно, какая часть данных упала;
- для каждой порции фиксируется время выполнения — так видно перекос нагрузки.
Типичные ошибки
- Пустой набор параметров. Порции подготовили, но не положили в соответствие — потоки не запустятся или результат будет пустым.
- Неэкспортный или клиентский метод. Имя метода должно указывать на экспортную функцию серверного общего модуля.
- Пересекающиеся порции при записи. Один документ в двух потоках — блокировки и «Данные были изменены другим пользователем».
- Слишком много потоков. После некоторого числа рост останавливается, а ожидания на СУБД растут. Замеряйте.
- Передача огромных объектов в параметрах. Параметры сериализуются; передавайте ссылки, а не объекты и не мегабайтные таблицы.
- Файловая база. Ускорения не будет, механизм рассчитан на клиент-серверный режим.
Параллелить стоит не «всё подряд», а независимую работу. Правильное деление на порции даёт больше, чем удвоение числа потоков.
Если обработка всё равно медленная, причина часто в запросах и блокировках, а не в количестве потоков. Это разбираю на аудите производительности 1С; сами многопоточные обработки делаю в рамках доработки 1С.
Итог
- Многопоточность БСП — это
ДлительныеОперации.ВыполнитьФункциюВНесколькоПотоковиВыполнитьПроцедуруВНесколькоПотоков. - Набор параметров — соответствие «ключ порции → массив параметров метода».
- Результат — соответствие по ключам порций, у каждой свой статус и адрес результата.
- Расчёт и запись разделяйте на два этапа, запись режьте по документам.
- С клиента ждите через
ДлительныеОперацииКлиент.ОжидатьЗавершение, на сервере — черезОжидатьЗавершение = Неопределено.




