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

21.08.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. Исходный код

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

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

 


#Область ПрограммныйИнтерфейс


// Выполнить экспортную процедуру по имени.
//
// Параметры:
//  ИмяЭкспортнойПроцедуры - Строка - имя экспортной процедуры в формате 
//                                    <имя объекта>.<имя процедуры>, где <имя объекта> - это
//                                    общий модуль или модуль менеджера объекта.
// Параметры               - Массив Из Тип - параметры передаются в процедуру <ИмяЭкспортнойПроцедуры>
//                                    в порядке расположения элементов массива.
// ОбластьДанных           - Число  - задает область данных, в которой необходимо выполнить процедуру.
// 
// Пример:
//  ВыполнитьБезопасно("МойОбщийМодуль.МояПроцедура");
//
Процедура ВыполнитьБезопасно(ИмяЭкспортнойПроцедуры, Параметры = Неопределено, ОбластьДанных = Неопределено) Экспорт
	
	// Проверка предусловий на формат ИмяЭкспортнойПроцедуры.       
	ЧастиИмени = РазделитьСтроку(ИмяЭкспортнойПроцедуры, ".");
	Если ЧастиИмени.Количество() <> 2 И ЧастиИмени.Количество() <> 3 Тогда
		ВызватьИсключение "Неправильный формат параметра ИмяЭкспортнойПроцедуры (" + ИмяЭкспортнойПроцедуры + ")";
	КонецЕсли;

	ИмяОбъекта = ЧастиИмени[0];
	Если ЧастиИмени.Количество() = 2 И Метаданные.ОбщиеМодули.Найти(ИмяОбъекта) = Неопределено Тогда
		ВызватьИсключение "Неправильный формат параметра ИмяЭкспортнойПроцедуры (" + ИмяЭкспортнойПроцедуры + ")";
	КонецЕсли;
		
	Если ЧастиИмени.Количество() = 3 Тогда
		ДопустимыеИменаТипов = Новый Массив;
		ДопустимыеИменаТипов.Добавить("КОНСТАНТЫ");
		ДопустимыеИменаТипов.Добавить("РЕГИСТРЫСВЕДЕНИЙ");
		ДопустимыеИменаТипов.Добавить("РЕГИСТРЫНАКОПЛЕНИЯ");
		ДопустимыеИменаТипов.Добавить("РЕГИСТРЫБУХГАЛТЕРИИ");
		ДопустимыеИменаТипов.Добавить("РЕГИСТРЫРАСЧЕТА");
		ДопустимыеИменаТипов.Добавить("СПРАВОЧНИКИ");
		ДопустимыеИменаТипов.Добавить("ДОКУМЕНТЫ");
		ДопустимыеИменаТипов.Добавить("ОТЧЕТЫ");
		ДопустимыеИменаТипов.Добавить("ОБРАБОТКИ");
		ДопустимыеИменаТипов.Добавить("БИЗНЕСПРОЦЕССЫ");
		ДопустимыеИменаТипов.Добавить("ЖУРНАЛЫДОКУМЕНТОВ");
		ДопустимыеИменаТипов.Добавить("ЗАДАЧИ");
		ДопустимыеИменаТипов.Добавить("ПЛАНЫСЧЕТОВ");
		ДопустимыеИменаТипов.Добавить("ПЛАНЫОБМЕНА");
		ДопустимыеИменаТипов.Добавить("ПЛАНЫВИДОВХАРАКТЕРИСТИК");
		ДопустимыеИменаТипов.Добавить("ПЛАНЫВИДОВРАСЧЕТА");
		ИмяТипа = ВРег(ЧастиИмени[0]);
		Если ДопустимыеИменаТипов.Найти(ИмяТипа) = Неопределено Тогда
			ВызватьИсключение "Неправильный формат параметра ИмяЭкспортнойПроцедуры (" + ИмяЭкспортнойПроцедуры + ")";
		КонецЕсли;
	КонецЕсли;
	
	ИмяМетода = ЧастиИмени[ЧастиИмени.ВГраница()];
	ВременнаяСтруктура = Новый Структура;
	Попытка
		// Проверка на то, что ИмяМетода является допустимым идентификатором.
		// Например: МояПроцедура
		ВременнаяСтруктура.Вставить(ИмяМетода);
	Исключение
		ЗаписьЖурналаРегистрации(НСтр("ru = 'Безопасное выполнение метода'"),
			УровеньЖурналаРегистрации.Ошибка, , , ОбработкаОшибок.ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));
		ВызватьИсключение "Неправильный формат параметра ИмяЭкспортнойПроцедуры (" + ИмяЭкспортнойПроцедуры + ")";
	КонецПопытки;
	
	ПараметрыСтрока = "";
	Если Параметры <> Неопределено И Параметры.Количество() > 0 Тогда
		Для Индекс = 0 По Параметры.ВГраница() Цикл 
			ПараметрыСтрока = ПараметрыСтрока + "Параметры[" + Индекс + "],";
		КонецЦикла;
		ПараметрыСтрока = Сред(ПараметрыСтрока, 1, СтрДлина(ПараметрыСтрока) - 1);
	КонецЕсли;
	
	//@skip-check server-execution-safe-mode
	Выполнить ИмяЭкспортнойПроцедуры + "(" + ПараметрыСтрока + ")";
	
КонецПроцедуры

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

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

	КонецЦикла;
	
	// ждем, пока все задания выполнятся или грохнутся - дальше другой код
	Настройки = НастройкиВыполнения(КоличествоПотоков);
	Настройки.ОбъединятьРезультаты = ОбъединятьРезультаты;
	ОбработкаОчередиЗаданий(АктивныеЗадания, Настройки, РезультатыВыполнения, Истина);
	Возврат РезультатыВыполнения;
	
КонецФункции

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

	Если ЭтоМассив И НастройкиВыполнения.ПередаватьТаблицуЗначений > 0 Тогда
		ВызватьИсключение "Использование простого массива для передачи в таблицу значений недоступно";
	КонецЕсли;
	Отметка = ТекущаяДатаСеанса();
	
	Для Счетчик = 1 По КоличествоДанных Цикл
		
		Если НастройкиВыполнения.ПередаватьТаблицуЗначений % 2 = 1 Тогда
			//@skip-check bsl-legacy-check-dynamic-feature-access
			Запись = ДанныеДляЗадания.Добавить();
		Иначе
			Запись = Новый Структура;
			Для Каждого КлючЗначение Из СтруктураЗаписи Цикл
				Запись.Вставить(КлючЗначение.Ключ);
			КонецЦикла;
		КонецЕсли;
		Если ЭтоВыборка Тогда
			МассивДанных.Следующий();
			ЗаполнитьЗначенияСвойств(Запись, МассивДанных);
		ИначеЕсли ЭтоМассив Тогда
			Запись = МассивДанных.Получить(Счетчик - 1);
		Иначе
			ЗаполнитьЗначенияСвойств(Запись, МассивДанных.Получить(Счетчик - 1));
		КонецЕсли;
		Если НастройкиВыполнения.ПередаватьТаблицуЗначений % 2 = 0 Тогда
			ДанныеДляЗадания.Добавить(Запись);
		КонецЕсли;
		Если Счетчик % НастройкиВыполнения.СтрокВПорции = 0 ИЛИ Счетчик = КоличествоДанных Тогда
			// проверка потоков
			БезаварийнаяОчередь = ОбработкаОчередиЗаданий(АктивныеЗадания, НастройкиВыполнения, РезультатыВыполнения);
			
			Если НастройкиВыполнения.ОстанавливатьПриАвариях И БезаварийнаяОчередь = Ложь Тогда
				Прервать;
			КонецЕсли;

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

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

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

// Функция - Массив записей в таблицу значений
// Параметры:
//  МассивЗаписей	 - Массив Из Структура - Массив однообразных структур для преобразования в таблицу
// 
// Возвращаемое значение:
//  ТаблицаЗначений - (Нет описания типов, в запрос не пойдет)
//
Функция МассивЗаписейВТаблицуЗначений(Знач МассивЗаписей) Экспорт
	
	Таблица = Новый ТаблицаЗначений;
	Если МассивЗаписей.Количество() > 0 Тогда
		ОписаниеКолонок = Новый Структура;
		Для Каждого КЗ Из МассивЗаписей[0] Цикл
			ОписаниеКолонок.Вставить(КЗ.Ключ, Новый Соответствие);
		КонецЦикла;
		Для Каждого Запись Из МассивЗаписей Цикл
			Для Каждого КЗ Из ОписаниеКолонок Цикл
				КЗ.Значение.Вставить(ТипЗнч(Запись[КЗ.Ключ]), 1);
			КонецЦикла;
		КонецЦикла;
		Для Каждого КЗ Из ОписаниеКолонок Цикл
			ТипыКолонки = Новый Массив;
			Для Каждого Запись Из КЗ.Значение Цикл
				ТипыКолонки.Добавить(Запись.Ключ);
			КонецЦикла;
			Таблица.Колонки.Добавить(КЗ.Ключ, Новый ОписаниеТипов(ТипыКолонки));
		КонецЦикла;
		Для Каждого Запись Из МассивЗаписей Цикл
			ЗаполнитьЗначенияСвойств(Таблица.Добавить(), Запись);
		КонецЦикла;
	КонецЕсли;
	Возврат Таблица;

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

// Функция - Описание объекта
//
// Параметры:
//  Объект		 - СправочникОбъект, ДокументОбъект, ДанныеФормыСтруктура - Преобразуемый в структуру объект
//  ТипЗначения	 - Тип - Тип значения объекта (нужен только для ДанныеФормыСтруктура, то есть реквизита формы)
// 
// Возвращаемое значение:
//  Структура - Структура с реквизитами и табличными частями объекта в виде таблиц значений
//
Функция ОписаниеОбъекта(Знач Объект, ТипЗначения = Неопределено) Экспорт

	ОписаниеОбъекта = Новый Структура;
	Если ТипЗнч(Объект) = Тип("ДанныеФормыСтруктура") Тогда
		Если ТипЗнч(ТипЗначения) <> Тип("Тип") Или СтрНайти(ВРег(ТипЗначения), "ОБЪЕКТ") = 0 Тогда
			ВызватьИсключение "Тип реквизита формы должен быть указан явно как ...Объект";
		КонецЕсли;
		Объект = ДанныеФормыВЗначение(Объект, ТипЗначения);
	КонецЕсли;
	
	МетаданныеОбъекта = Объект.Метаданные();
		
	Для Каждого Реквизит Из МетаданныеОбъекта.СтандартныеРеквизиты Цикл
		ОписаниеОбъекта.Вставить(Реквизит.Имя, Объект[Реквизит.Имя]);
	КонецЦикла;
	
	Для Каждого Реквизит Из МетаданныеОбъекта.Реквизиты Цикл
		ОписаниеОбъекта.Вставить(Реквизит.Имя, Объект[Реквизит.Имя]);
	КонецЦикла;
	
	Для Каждого ТабличнаяЧасть Из МетаданныеОбъекта.ТабличныеЧасти Цикл
		ОписаниеОбъекта.Вставить(ТабличнаяЧасть.Имя, Объект[ТабличнаяЧасть.Имя].Выгрузить());
	КонецЦикла;
	
	Возврат ОписаниеОбъекта;
	
КонецФункции

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

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

// Функция - Соединить строку разделителем - Для совместимости
//
// Параметры:
//  ЧастиСтроки				 - Строка - Входная строка к разделению по разделителю
//  Разделитель	 		- Строка - Набор символов, разделяющих строку на части
// 
// Возвращаемое значение:
// Строка  - Строка после соединения массива
//
Функция СоединитьСтроку(Знач ЧастиСтроки, Знач Разделитель) Экспорт
	
	Слово = "";
	Для Индекс = 1 По ЧастиСтроки.Количество() Цикл
		Слово = Слово + ЧастиСтроки[Индекс - 1] + Разделитель;
	КонецЦикла;
	Слово = Лев(Слово, СтрДлина(Слово) - СтрДлина(Разделитель));
	Возврат Слово;
	
КонецФункции

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

	КонецЦикла;
	Возврат МассивЗаписей;
	
КонецФункции

#КонецОбласти

#Область СлужебныйПрограммныйИнтерфейс

// Функция - Настройки выполнения
//
// Параметры:
//  КоличествоПотоков		 - Число - Количество разрешенных потоков одновременно запускаемых фоновых заданий
//  СтрокВПорции			 - Число - 0 ИЛИ Число строк из массива данных на одно фоновое задание
//  ОстанавливатьПриАвариях	 - Булево - Останавливать ли выполнение всех заданий при ошибке в одном из них,
//										при отмене одного из заданий, отменяются и все последующие
//  ВозвращатьРезультаты	 - Булево - Собирать и возвращать результаты выполнения, сохраненные по адресу хранилища
// 
// Возвращаемое значение:
//  Структура - Структура со свойствами: 
//   * КоличествоПотоков - Число - Количество разрешенных потоков одновременно запускаемых фоновых заданий
//   * ВыполнятьБезопасноИлиИзМенеджера - Булево - Выполнять напрямую (только общие модули) или через менеджер
//   * ОбъединятьРезультаты - Булево - Объединять ли результаты выполнения в один массив
//   * СтрокВПорции - Число - 0 ИЛИ Число строк из массива данных на одно фоновое задание 
//	 * ОстанавливатьПриАвариях - Булево - Останавливать ли выполнение всех заданий при ошибке в одном из них
//	 * ВозвращатьРезультаты - Булево - Собирать и возвращать результаты выполнения, сохраненные по адресу хранилища
//	 * ПотокЖурналаРегистрации - Строка - Имя потока журнала регистрации
//	 * ПередаватьТаблицуЗначений - Число - 0 - Передавать массив (структур, например), 1 - передавать простую ТЗ,
//	   		2 - таблицу значений с описаниями типов
//
Функция НастройкиВыполнения(КоличествоПотоков, СтрокВПорции = 0, ОстанавливатьПриАвариях = Ложь,
		ВозвращатьРезультаты = Истина) Экспорт
		
	Настройки = Новый Структура("КоличествоПотоков, СтрокВПорции", КоличествоПотоков, СтрокВПорции);
	Настройки.Вставить("ОстанавливатьПриАвариях", ОстанавливатьПриАвариях);
	Настройки.Вставить("ВозвращатьРезультаты", ВозвращатьРезультаты);
	Настройки.Вставить("ВыполнятьБезопасноИлиИзМенеджера", Истина);
	Настройки.Вставить("ОбъединятьРезультаты", Истина);
	Настройки.Вставить("ПотокЖурналаРегистрации", "Обработка заданий порциями");
	Настройки.Вставить("ПередаватьТаблицуЗначений", 0);
	Возврат Настройки;
	
КонецФункции

#КонецОбласти

#Область СлужебныеПроцедурыИФункции

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

		КонецЦикла; // АктивныеЗадания
		
		// запуск паузы или отмена всего, если что-то пошло не так
		Если ОбнаруженаАвария Тогда
			Если НастройкиВыполнения.ОстанавливатьПриАвариях Тогда
				Для Каждого Запись Из АктивныеЗадания Цикл
					ФоновыеЗадания.НайтиПоУникальномуИдентификатору(Запись.ИдентификаторФЗ).Отменить();
				КонецЦикла;
				АктивныеЗадания.Очистить();
			Иначе	
				// ждем 3 минуты, возможно, сервер просто крепко занят
				Пауза(ПаузаПриОшибке);
			КонецЕсли;
		Иначе
			Если АктивныеЗадания.Количество() >= КоличествоПотоков Тогда
				Пауза(ПаузаПриОбработкеОчереди);
			КонецЕсли;
		КонецЕсли;
		ПроверкаВыполнена = Истина;
	КонецЦикла; // КоличествоПотоков
	
	Возврат ОчередьБезаварийная;
	
КонецФункции

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

Процедура ПополнитьРезультаты(Знач Адрес, Знач ОбъединятьРезультаты, Знач РезультатыВыполнения)
	
	Если ЭтоАдресВременногоХранилища(Адрес) И РезультатыВыполнения <> Неопределено Тогда
		РезультатыЗадания = ПолучитьИзВременногоХранилища(Адрес);
		Если РезультатыЗадания <> Неопределено Тогда
			Если ОбъединятьРезультаты Тогда
				Для Каждого Результат Из РезультатыЗадания Цикл
					РезультатыВыполнения.Добавить(Результат);
				КонецЦикла;
			Иначе
				РезультатыВыполнения.Добавить(РезультатыЗадания);
			КонецЕсли;
		КонецЕсли;
	КонецЕсли;

КонецПроцедуры


#КонецОбласти

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

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

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

Вы можете заказать платную адаптацию этой статьи под ваши задачи на «Бирже заказов».

  • 0% комиссии — оплата напрямую исполнителю;
  • Исполнители любого масштаба — от отдельных специалистов до команд под проект;
  • Прямой обмен контактами между заказчиком и исполнителем;
  • Безопасная сделка — при необходимости;
  • Рейтинги, кейсы и прозрачная система откликов.

См. также

Механизмы платформы 1С Разработчик Бесплатно (free)

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

16.07.2025    44464    TitanLuchs    109    

153

Работа с интерфейсом WEB-интеграция Механизмы платформы 1С Бесплатно (free)

Веб-интерфейсы повышают качество внешнего вида приложений 1С, предсказуемы с точки зрения верстки, позволяют организовать удобные и высокопроизводительные рабочие места для пользователей. Расскажем об особенностях разработки веб-интерфейсов на React внутри 1С, двустороннем взаимодействии 1С и JavaScript, а также сборке веб-приложения в одностраничный файл.

19.06.2025    17748    zeegin    15    

62

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

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

03.02.2025    25353    bayselonarrend    127    

68

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

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

30.01.2025    32031    user2122906    10    

68

Механизмы платформы 1С Файловый обмен (TXT, XML, DBF), FTP Разработчик 1С:Предприятие 8 Бесплатно (free)

Этот материал познакомит вас с механизмом XDTO (XML Data Transfer Objects) в 1С и научит эффективно использовать его возможности. Мы разберёмся, как работать с XML-схемами, создавать модели данных, манипулировать объектами XDTO, а также сериализовать и десериализовать их в XML. Вы узнаете, как использовать XDTO для интеграции с внешними системами, избегать типичных ошибок и оптимизировать код. К концу вы будете уверенно применять XDTO для решения сложных задач обмена данными и автоматизации процессов.

17.01.2025    53171    user2122906    13    

62

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

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

14.01.2025    45309    dsdred    110    

155

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

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

23.06.2024    39490    bayselonarrend    22    

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

Многопоточность зависит от структуры базы и запросов к ней, и от индексов и от плана запроса и т.д.
2. n_mezentsev 169 10.02.26 11:27 Сейчас в теме
(1) Подразумевается, что порции несвязанные, а все необходимые данные уже есть. По поводу блокировок. Сама запись обычно процедура очень быстрая, как-то в один миллионный регистр писал до 30 потоков, никаких проблем не наблюдал, да и в проблемной базе, где запись в тот же регистр шла до 1.5-2 секунд блокировки ловил крайне редко - возможно, потому что писал рекомендуемым МенеджеромЗаписи, не знаю. В примере в демобазе пишется справочник в 15 потоков - никаких проблем, при том что параллельно еще идут операции поиска по справочнику. Проблемы блокировки начинаются когда? Когда транзакция записи уже началась, а алгоритм тормозит выполнение на неопределенное время, поэтому в ERP сперва заполняются таблицы движений, потом идет запись - здесь тот же принцип сработает также.
3. IgorVasilyev 146 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. IgorVasilyev 146 10.02.26 17:25 Сейчас в теме
(4) Согласен, если смотреть на это именно как на работу с физическим порядком данных, то разбиение по ключу кластеризованного индекса — практически единственный осмысленный вариант для масштабируемого параллелизма. В этом смысле порции — это уже не просто «кусочки массива», а попытка минимизировать конкуренцию за страницы.
Но мне тут кажется важным одно уточнение. Даже если 1С сама по себе «нетороплива», она всё равно умеет достаточно эффективно создавать конкуренцию на уровне СУБД — просто за счёт количества сеансов. И рост PAGELATCH_EX часто прилетает не из-за скорости записи, а из-за того, что несколько потоков начинают регулярно встречаться на одних и тех же горячих участках индекса.
Именно поэтому сортировка по случайному признаку (или условный NEWID()) — палка о двух концах: да, мы размазываем обращения по страницам, но при этом теряем предсказуемость доступа и часто платим за это планом запроса и лишними чтениями. В одних задачах это спасает, в других — наоборот, добивает.
По ощущениям, самый сложный момент здесь даже не в выборе стратегии разбиения, а в том, что граница «ещё ускоряем» → «уже деградируем» почти всегда определяется экспериментально и сильно зависит от конкретной схемы индексов и нагрузки базы. Универсального рецепта, увы, так и не появилось.
8. booksfill 10.02.26 18:04 Сейчас в теме
(7)
и часто платим за это планом запроса и лишними чтениями

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

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

А распараллеливание чисто вычислительных задач, не работающих с СУБД, с разбивкой на потоки...
Даже и не знаю, тогда лучше уж смотреть в строну микросервисов на предназначенных для такой работы языках.
Да, теряем время на передачу (например через брокер) данных, но скорость обработки информации будет на порядки быстрей, да и пожирания серверных лицензий не будет (вроде как фоновое ест серверные) + множество нужных библиотек под рукой.
9. IgorVasilyev 146 10.02.26 18:48 Сейчас в теме
(8) Да, вот тут, по-моему, и проходит ключевая граница применимости. Как только в схеме появляется обязательный SELECT перед записью (а в 1С он почти всегда есть), мы просто переносим нагрузку: снимаем конкуренцию на INSERT/UPDATE и получаем её на чтении, часто ещё и с менее предсказуемым планом.
С вычислительными задачами без СУБД согласен почти полностью. В таких случаях фоновые задания в 1С — это скорее «вынужденный компромисс», чем оптимальный инструмент. Они удобны, когда нужно остаться внутри платформы и данных, но как только задача становится реально CPU-bound, стоимость сеанса и сериализаций начинает доминировать.
Поэтому на практике у меня обычно получается так:
если задача тесно связана с данными 1С и транзакционной моделью — играем конвейером, порциями и аккуратным контролем конкуренции, если это чистая математика / перебор / анализ — гораздо логичнее выносить наружу, даже с учётом брокеров и задержек на передачу. Потери на интеграции окупаются предсказуемостью и масштабируемостью.
В этом смысле статья как раз хороша тем, что показывает цену фоновых заданий, а не продаёт их как универсальное решение.
11. n_mezentsev 169 10.02.26 19:21 Сейчас в теме
(8)
вроде как фоновое ест серверные
не трогает фоновое задание лицензии! Проверял на 30 тысячах фоновых на комьюнити лицензии)
14. starik-2005 3303 11.02.26 11:03 Сейчас в теме
(11)
Проверял на 30 тысячах фоновых на комьюнити лицензии
Ужос-кошмар...
15. n_mezentsev 169 11.02.26 11:10 Сейчас в теме
(14) Это было случайно - Рекурсия! Мать диких обезьян)
5. n_mezentsev 169 10.02.26 16:45 Сейчас в теме
(3) Если беспокоит именно запись, ничего не мешает вернуть готовые к записи данные и уже в однопоточном режиме записать их, возможно, уменьшая "взаимные" блокировки, но не уменьшая общую нагрузку на систему (будет записан тот же объем информации). Но все-таки согласитесь, что обычно эффект от многопоточности будет достигнут не "за счет" высококонкурентной записи, а за счет разлива нагрузки вычислительного алгоритма между потоками и общая отзывчивость системы в этот момент может страдать (у меня в примерах заняты на 100% 16 ядер из 18), но нужно понимать, что это сконцентрированная нагрузка, которая длится в разы меньше чем однопоточная, но на частых операциях злоупотреблять я бы все равно не стал
6. IgorVasilyev 146 10.02.26 17:11 Сейчас в теме
(5) Да, здесь полностью согласен. Если вынести запись в однопоточный этап, то мы по сути просто меняем профиль нагрузки, а не её объём — выигрываем в управляемости блокировок, но не в «дешевизне» операции.
Мне кажется, ключевая мысль как раз в том, что многопоточность в таких сценариях почти всегда покупается за счёт ухудшения отзывчивости системы здесь и сейчас. И тогда вопрос уже не «быстрее или медленнее», а «какой профиль деградации мы считаем допустимым». Концентрированная нагрузка на CPU на коротком интервале зачастую оказывается менее болезненной, чем растянутая однопоточная, но это работает ровно до тех пор, пока задача не становится регулярной.
В этом смысле очень откликается мысль «не злоупотреблять на частых операциях». По ощущениям, такие конвейеры хорошо ложатся на редкие, но тяжёлые расчёты, а вот вблизи OLTP-нагрузки граница применимости наступает очень быстро — даже без явных блокировок и ошибок.
Интересно, рассматривали ли вы в подобных схемах динамическое ограничение параллелизма (например, в зависимости от времени суток или текущей загрузки), или считаете, что это уже избыточное усложнение?
10. n_mezentsev 169 10.02.26 19:18 Сейчас в теме
(6) Интересный вопрос. Но он у меня не возникал, наверное потому, что разрабатывал это для обычных форм и толстого клиента, где сервер не воспринимается как что-то вечно загруженное работой. А так, на ум приходит короткий, но по возможности синтетический тест пускай в однопотоке с хранением результатов в общем хранилище для определения того, насколько можно дозагрузить систему. Думаю, было бы интересно, сможет ли какой-либо тест определить, что сейчас идет операция закрытия месяца)
12. IgorVasilyev 146 10.02.26 19:33 Сейчас в теме
(10) Да, понимаю этот контекст — когда сервер воспринимается как «ресурс под конкретную операцию», а не как постоянно нагруженная платформа. В таких условиях действительно проще мыслить статическими лимитами и не усложнять механику.
Мне, правда, кажется, что как только 1С начинает жить в режиме смешанной нагрузки (OLTP + регламентные + фоновые), вопрос динамики всё равно всплывает, даже если изначально его не закладывали. Причём не столько в виде «умного адаптивного алгоритма», сколько в виде очень грубых правил: время суток, тип операции, признак «регламентная/пользовательская».
Идея синтетического теста откликается, но, по ощущениям, он скорее хорошо ловит средний профиль нагрузки, чем пики. Закрытие месяца или массовые перепроведения как раз тем и коварны, что деградация там начинается не с CPU, а с очередей ожиданий, и тест может просто не успеть это отразить.
В итоге у меня чаще всего всё сводилось к довольно приземлённому компромиссу: фиксированные лимиты плюс организационные правила запуска тяжёлых конвейеров. Не потому что это красиво, а потому что в реальной эксплуатации предсказуемость часто оказывается важнее адаптивности.
Интересно, кстати, где у вас на практике проходила грань, после которой сервер начинал «чувствоваться» пользователями — по CPU, по времени ответа, по количеству активных сеансов?
13. starik-2005 3303 11.02.26 10:23 Сейчас в теме
Статья хорошая.
То есть видно, что удвоение числа потоков не приводит к удвоению скорости в самом простом случае (хотя ядер, как я говорил, у меня с запасом), а при большом объеме входных данных даже замедляет процесс, поскольку накладные расходы уже превышают внутренние.
Проблемы в том, что работа с памятью на пользовательских компах, если они собраны хорошо (т.е. модули вставлены в правильные слоты и в BIOS все настроено на хотя бы заявленные характеристики комплекта), ограничена двумя потоками. И как только вам перестает хватать кеша, у вас резко проседает производительность. Т.е. при активном чтении больших кусков памяти, особенно непоследовательно расположенных, два потока будут работать лучше одного, а вот уже три - не факт. Да и в два-то потока память работает благодаря технологии interleave, которая позволяет располагать адресное пространство кусками поочередно по 256-4096 байт из каждого модуля.
16. n_mezentsev 169 11.02.26 11:20 Сейчас в теме
(13) Я согласен с тем, что в работе с железом ограничения определенно есть, но что зависимость от "многопоточности" памяти прямая не согласен, ведь все зависит от нагрузки и скорости передачи информации, которая от года в год растет. Ну а так, я к сожалению, не знаю как работает сервер 1С с фоновыми сеансами, наверняка это отдельные потоки, которые должны активно обмениваться к обслуживающими их потоками, которые тоже должны поспевать за возросшей нагрузкой - так что так, думаю и на чуть более высоком уровне также все за то, чтобы двойного выигрыша не было
17. starik-2005 3303 11.02.26 11:40 Сейчас в теме
(16)
чтобы двойного выигрыша не было
Ну вот я еще лет под сотню назад тестил параллельную запись на каком-то линуховом компе с восмиядерным (условно) процом и двухканальной памятью. Так вот если отрубить кеш записи на диск, то файловая система (ext2 в то время у меня было) объединяла записываемые блоки очень эффективно, поэтому скорость росла аж до ста потоков. А вот как только я включал кеш на запись, то картина в корне менялась - максимальная производительность достигалась на 3-х потоках, ну может быть 4-х. Порции были маленькие, в кеш в общем-то влезали. Узким местом для варианта без кеша записи на диск был явно диск, а он работает медленнее на несколько порядков, чем память (да, тогда еще HDD был), а вот когда кеш врубился, то все стало упираться в память. То самое бутылочное горлышко. Правильные посоны сейчас вообще пытаются работать с балансом во всяких там реально высокопроизводительных системах. Та же память HBM имеет высокую пропускную способность в кучи ТБ/с за счет широкой шины в окулиард бит, что позволяет эффективно параллельно работать с огромными матрицами ИИ-моделей на куче тензорных и прочими словами названных процессоров. А обычный ПиСи ограничен двумя каналами памяти с пропускной способностью последовательного чтения в лучшем случае в 100 Гб/с. Так что уже два ядра будут конкурировать за ресурсы, если память не влезает в кеш. Из-за этого медленные процы с большим количеством кеша рвут более быстрые процы с небольшим этого кеша количеством в большинстве софта, ибо кеш работает на куда более высокой частоте, чем память. В итоге у нас есть цепочка бутылочных горлышек: IOPS -> память -> кеш (3/2/1 уровня) -> IPC проца.
n_mezentsev; +1 – Ответить
18. aximo 2750 11.02.26 14:47 Сейчас в теме
статья безусловно полезная... кстати, тут отметили, что в потоках могут выполняться разноплановые операции и еще я тут не увидел слов "БСП и т.п.... или я плохо смотрел?
19. starik-2005 3303 11.02.26 14:51 Сейчас в теме
(18)
БСП
Ну видимо как-то так:
ДлительныеОперацииСервер.ВыполнитьФункциюПорциями
20. n_mezentsev 169 11.02.26 18:49 Сейчас в теме
(18) Как раз БСП здесь и нет, потому что там сложно и, как мне показалось, идет разбиение не на порции данных, а на единичные наборы (возможно заблуждаюсь), так что конвейер свой доработанный "общественный" с инфостарта. А ДлительныеОперацииСервер - ну надо было для сравнения/объединения между базами назвать - назвал по аналогии с БСП (имя свободно)
Для отправки сообщения требуется регистрация/авторизация