Использование типового механизма многопоточности БСП
В продолжении статьи «Ускорение обработки данных в 1С» возникла мысль описать как многопоточное выполнение реализовано в текущей версии БСП.
БСП развивается, постоянно меняется. На данный момент я реализовывал многопоточный функционал на базе БСП 3.1.9. Предыдущая версия отличается по параметрам рассматриваемых функций. Статья будет полезна как начинающим разработчикам, так и продвинутым разработчикам, которых эта тема еще не коснулась.
Contents
Чем будем пользоваться
В данной версии БСП в общем модуле «ДлительныеОперации» есть функции «ВыполнитьФункциюВНесколькоПотоков» и «ВыполнитьПроцедуруВНесколькоПотоков». У них есть три параметра:
- <ИмяФункции [ИмяПроцедуры]>,
- <ПараметрыВыполнения>,
- <НаборПараметровФункции [НаборПараметровПроцедуры] = Неопределено>.
ИмяФункции [ИмяПроцедуры] – имя функции или процедуры, в которой будет код обработки порции многопоточного выполнения.
ПараметрыВыполнения – структура, параметры многопоточного выполнения. Для облегчения работы можно получить функциями ДлительныеОперации. ПараметрыВыполненияФункции(ИдентификаторФормы) и ДлительныеОперации. ПараметрыВыполненияПроцедуры().
НаборПараметровФункции [НаборПараметровПроцедуры] – соответствие, где ключ произвольное значение, некоторое обобщение данной порции многопоточного выполнения. В общем случае может быть номером порции, или ссылкой на документ, если у нас идет разбивка порций по документам. В значение соответствия помещаем массив параметров функции, описанной в первом параметре. Количество параметров не должно быть больше семи.
Обе функции возвращают одинаковое значение – структуру, в которой описывается результат ее выполнения. Подробнее разберем в статье.
Принцип работы
Перед началом реализации многопоточной работы нужно определится: будем обрабатывать результат или достаточно запустить многопоточное выполнение и забыть про него. В зависимости от этого будем пользоваться ВыполнитьФункциюВНесколькоПотоков в первом случае и ВыполнитьПроцедуруВНесколькоПотоков во втором.
Для работы с многопоточным выполнением нам необходимо создать как минимум две экспортные функции в серверном модуле.
Первая функция для подготовки и запуска многопоточного выполнения. В ней необходимо подготовить параметры выполнения и порции для последующего помещения их в функцию ВыполнитьФункциюВНесколькоПотоков [ВыполнитьПроцедуруВНесколькоПотоков].
Вторая функция будет непосредственно обрабатывать порции.
Если мы запускаем многопоточность с клиента и нам необходимо отслеживать ее выполнение, то важно на клиенте создать третью функцию, в которой будет запушена функция формирования порций.
Если нужна постобработка данных сформированных многопоточностью (их тоже можно обработать многопоточно), то необходима четвертая серверная функция.
Работа на клиенте с постобработкой
Рассмотрим вариант запуска с клиента. У нас есть массив документов, необходимо посчитать какие-то данные, записать их в табличные части документов и записать документы. При этом видим форму длительного задания, сигнализирующую о том, что процесс выполняется. Делим многопоточность на два раза. Это необходимо, так как в первом случае мы выполняем расчет, который не зависит от документов. Во втором запуске многопоточности мы заполняем и записываем документ. Если разделим его в разные потоки, то можем получить неправильное заполнение. Например, при последовательном запуске потоков по документу. Если потоки будут запущены параллельно, то можем получить блокировку.
Процедура запуска многопоточности на клиенте:
Процедура РассчитатьДокументыМногопоточноКлиент(ФормаВладелец, ПериодРасчета) Экспорт
Задание = РасчетДокументовСервер.РассчитатьДокументыМногопоточно(ПериодРасчета);
Если Задание = Неопределено Тогда
Возврат;
КонецЕсли;
НастройкиОжидания = ДлительныеОперацииКлиент.ПараметрыОжидания(ФормаВладелец);
НастройкиОжидания.ТекстСообщения = НСтр("ru = 'Идет расчет документов.'");
НастройкиОжидания.ВыводитьОкноОжидания = Истина;
НастройкиОжидания.ВыводитьПрогрессВыполнения = Ложь;
НастройкиОжидания.Интервал = 0;
НастройкиОжидания.ВыводитьСообщения = Ложь;
Обработчик = Новый ОписаниеОповещения("РассчитатьДокументыЗавершение", ЭтотОбъект, ФормаВладелец);
ДлительныеОперацииКлиент.ОжидатьЗавершение(Задание, Обработчик, НастройкиОжидания);
КонецПроцедуры
Нужна для вывода формы ожидания.
Функция запуска многопоточности на сервере:
Функция РассчитатьДокументыМногопоточно(ПериодРасчета, ЧислоПотоков = 10) Экспорт
ПараметрыМетода = Новый Соответствие;
Порции = Новый Массив;
Для ИндексПорции = 0 По ЧислоПотоков Цикл
Порции.Добавить(Новый Массив);
КонецЦикла;
ДанныеДляРасчета = РасчетДокументовСервер.ПодготовитьДанныеДляРасчета(ПериодРасчета);
Пока ДанныеДляРасчета.Количество() Цикл
Для ИндексПорции = 0 По ЧислоПотоков Цикл
ПоследняяЗапись = ДанныеДляРасчета.ВГраница();
Порции[ИндексПорции].Добавить(ДанныеДляРасчета[ПоследняяЗапись]);
ДанныеДляРасчета.Удалить(ПоследняяЗапись);
Если ДанныеДляРасчета.Количество() = 0 Тогда
Прервать;
КонецЕсли;
КонецЦикла;
КонецЦикла;
ИмяФункции = "РасчетДокументовСервер.РассчитатьДокументы";
НаименованиеМетода = НСтр("ru = 'Расчет документов.'");
ПараметрыВыполнения = ДлительныеОперации.ПараметрыВыполненияФункции(Новый УникальныйИдентификатор());
ПараметрыВыполнения.НаименованиеФоновогоЗадания = НаименованиеМетода;
ПараметрыВыполнения.ОжидатьЗавершение = 0;
АдресРезультата = ПоместитьВоВременноеХранилище(Неопределено, Новый УникальныйИдентификатор());
ПараметрыВыполнения.Вставить("АдресРезультата", АдресРезультата);
Возврат ДлительныеОперации.ВыполнитьФункциюВНесколькоПотоков(ИмяФункции, ПараметрыВыполнения, ПараметрыМетода);
КонецФункции
Собственно, сам запуск многопоточности.
Функция расчета:
Функция РассчитатьДокументы(ПорцияДанных) Экспорт
Результат = Новый ТаблицаЗначений;
//... Тут делаем расчет ...
Возврат Результат;
КонецФункции
Процедура постобработки:
Процедура РассчитатьДокументыЗавершение(Результат, ФормаВладелец) Экспорт
Если Результат = Неопределено Тогда
Возврат;
КонецЕсли;
Если Результат.Статус <> "Выполнено" Тогда
// Фоновое задание завершено с ошибкой.
Кратко = НСтр("ru = 'Не удалось рассчитать документы по причине:'");
Кратко = Кратко + Символы.ПС + Результат.КраткоеПредставлениеОшибки;
Подробно = Кратко + Символы.ПС + Символы.ПС + Результат.ПодробноеПредставлениеОшибки;
ОбщегоНазначенияКлиент.СообщитьПользователю(Подробно);
Возврат;
КонецЕсли;
Порции = ПолучитьИзВременногоХранилища(Результат.АдресРезультата);
Для каждого КиЗ Из Порции Цикл
ПараметрыПорции = КиЗ.Значение;
Если ПараметрыПорции.Статус <> "Выполнено" Тогда
// Фоновое задание порции завершено с ошибкой.
Кратко = НСтр("ru = 'Не удалось рассчитать документы порции %1 по причине:'");
Кратко = Кратко + Символы.ПС + Результат.КраткоеПредставлениеОшибки;
Подробно = Кратко + Символы.ПС + Символы.ПС + Результат.ПодробноеПредставлениеОшибки;
ОбщегоНазначенияКлиент.СообщитьПользователю(СтрШаблон(Подробно, КиЗ.Ключ));
Продолжить;
КонецЕсли;
КонецЦикла;
ЗаполнитьИЗаписатьДокументыМногопоточноНаКлиенте(ФормаВладелец, Порции)
КонецПроцедуры
Так как начали с клиента, то и продолжим на клиенте, снова запустим многопоточность.
Процедура запуска второй многопоточности, без постобработки:
Процедура ЗаполнитьИЗаписатьДокументыМногопоточноНаКлиенте(ФормаВладелец, ПорцииРасчета) Экспорт
//Эту функцию расписывать не буду, возвращает соответствие, где
//ключ -документ, а значение массив структур для записи.
ДанныеДляЗаписи = СобратьПорцииНаСервере(ПорцииРасчета);
Задание = РасчетДокументовСервер.ЗаполнитьИЗаписатьДокументыМногопоточно(ДанныеДляЗаписи);
Если Задание = Неопределено Тогда
Возврат;
КонецЕсли;
НастройкиОжидания = ДлительныеОперацииКлиент.ПараметрыОжидания(ФормаВладелец);
НастройкиОжидания.ТекстСообщения = НСтр("ru = 'Идет заполнение и запись документов.'");
НастройкиОжидания.ВыводитьОкноОжидания = Истина;
НастройкиОжидания.ВыводитьПрогрессВыполнения = Ложь;
НастройкиОжидания.Интервал = 0;
НастройкиОжидания.ВыводитьСообщения = Ложь;
ДлительныеОперацииКлиент.ОжидатьЗавершение(Задание, , НастройкиОжидания);
КонецПроцедуры
Функция запуска второй многопоточности на сервере:
Функция ЗаполнитьИЗаписатьДокументыМногопоточно(ДанныеДляЗаписи, ЧислоПотоков = 10) Экспорт
ПараметрыМетода = Новый Соответствие;
ЧислоПотоков = ?(ЧислоПотоков = 0, 1, ЧислоПотоков);
ДокументовВПотоке = Цел(ДанныеДляЗаписи / ЧислоПотоков);
Докуметы = Новый Массив;
Для каждого КиЗ Из ДанныеДляЗаписи Цикл
Докуметы.Добавить(КиЗ.Ключ);
КонецЦикла;
Для ИндексПорции = 1 По ЧислоПотоков Цикл
ДокВПотоке = 0;
Порция = Новый Соответствие;
Для каждого Документ Из Докуметы Цикл
ДокВПотоке = ДокВПотоке + 1;
Значение = ДанныеДляЗаписи[Документ];
Порция.Вставить(Документ, Значение);
ДанныеДляЗаписи.Удалить(Документ);
Если ДокВПотоке = ДокументовВПотоке Или ДанныеДляЗаписи.Количество() = 0 Тогда
Прервать;
КонецЕсли;
КонецЦикла;
ТекПараметры = Новый Массив(Порция);
ПараметрыМетода.Вставить(ИндексПорции, ТекПараметры)
КонецЦикла;
ТекПараметры = ПараметрыМетода[ИндексПорции];
Порция = ТекПараметры[0];
Для каждого КиЗ Из ДанныеДляЗаписи Цикл
Порция.Вставить(КиЗ.Ключ, КиЗ.Значение);
КонецЦикла;
ПараметрыМетода[ИндексПорции] = ТекПараметры;
ИмяПроцедуры = "РасчетДокументовСервер.ЗаполнитьИЗаписатьДокументы";
НаименованиеМетода = НСтр("ru = 'Заполнение и запись документов.'");
ПараметрыВыполнения = ДлительныеОперации.ПараметрыВыполненияПроцедуры();
ПараметрыВыполнения.НаименованиеФоновогоЗадания = НаименованиеМетода;
ПараметрыВыполнения.ОжидатьЗавершение = 0;
АдресРезультата = ПоместитьВоВременноеХранилище(Неопределено, Новый УникальныйИдентификатор());
ПараметрыВыполнения.Вставить("АдресРезультата", АдресРезультата);
Возврат ДлительныеОперации.ВыполнитьПроцедуруВНесколькоПотоков(ИмяПроцедуры, ПараметрыВыполнения, ПараметрыМетода);
КонецФункции
Функцию записи не описываю. Понятно, что у нее будет один параметр с соответствием документа и данных заполнения.
Работа на сервере с постобработкой
Разберем все тоже самое, но без клиента, например, при запуске из регламентного задания.
Функция многопоточности на сервере:
Функция РассчитатьДокументыМногопоточно(ЧислоПотоков = 10) Экспорт
ПериодРасчета = Новый СтандартныйПериод;
ПериодРасчета.ДатаНачала = НачалоМесяца(ТекущаяДатаСеанса());
ПериодРасчета.ДатаОкончания = КонецМесяца(ТекущаяДатаСеанса());
ПараметрыМетода = Новый Соответствие;
Порции = Новый Массив;
Для ИндексПорции = 0 По ЧислоПотоков Цикл
Порции.Добавить(Новый Массив);
КонецЦикла;
ДанныеДляРасчета = РасчетДокументовСервер.ПодготовитьДанныеДляРасчета(ПериодРасчета);
Пока ДанныеДляРасчета.Количество() Цикл
Для ИндексПорции = 0 По ЧислоПотоков Цикл
ПоследняяЗапись = ДанныеДляРасчета.ВГраница();
Порции[ИндексПорции].Добавить(ДанныеДляРасчета[ПоследняяЗапись]);
ДанныеДляРасчета.Удалить(ПоследняяЗапись);
Если ДанныеДляРасчета.Количество() = 0 Тогда
Прервать;
КонецЕсли;
КонецЦикла;
КонецЦикла;
ИмяФункции = "РасчетДокументовСервер.РассчитатьДокументы";
НаименованиеМетода = НСтр("ru = 'Расчет документов.'");
ПараметрыВыполнения = ДлительныеОперации.ПараметрыВыполненияФункции(Новый УникальныйИдентификатор());
ПараметрыВыполнения.НаименованиеФоновогоЗадания = НаименованиеМетода;
ПараметрыВыполнения.ОжидатьЗавершение = 0;
АдресРезультата = ПоместитьВоВременноеХранилище(Неопределено, Новый УникальныйИдентификатор());
ПараметрыВыполнения.Вставить("АдресРезультата", АдресРезультата);
Результат = ДлительныеОперации.ВыполнитьФункциюВНесколькоПотоков(ИмяФункции, ПараметрыВыполнения, ПараметрыМетода);
АдресХранилища = Результат.АдресХранилища;
Если Результат.Статус <> "Выполнено" Тогда
Пока Результат.Статус = "Выполняется" Цикл
Результат = ДлительныеОперации.ЗаданиеВыполнено(Результат.ИдентификаторЗадания, Истина);
КонецЦикла;
КонецЕсли;
Если Результат.Статус = "Ошибка" Тогда
//Тут обрабатываем ошибки.
ИначеЕсли Результат.Статус = "Отменено" Тогда
//Тут обрабатываем отмену.
ИначеЕсли Результат.Статус = "Выполнено" Тогда
ПорцииРасчета = ПолучитьИзВременногоХранилища(АдресРезультата);
//Эту функцию расписывать не буду, возвращает соответствие, где
//ключ -документ, а значение массив структур для записи.
ДанныеДляЗаписи = СобратьПорцииНаСервере(ПорцииРасчета);
РасчетДокументовСервер.ЗаполнитьИЗаписатьДокументыМногопоточно(ДанныеДляЗаписи);
КонецЕсли;
КонецФункции
Все остальные функции описаны выше.
Comments
So empty here ... leave a comment!