Top.Mail.Ru
График: 5/2, full-time
Формат: удаленный/офис
Вакансия «1С-программист»

Использование типового механизма многопоточности БСП

В продолжении статьи «Ускорение обработки данных в 1С» возникла мысль описать как многопоточное выполнение реализовано в текущей версии БСП.

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

Чем будем пользоваться

В данной версии БСП в общем модуле «ДлительныеОперации» есть функции «ВыполнитьФункциюВНесколькоПотоков» и «ВыполнитьПроцедуруВНесколькоПотоков». У них есть три параметра:

  1. <ИмяФункции [ИмяПроцедуры]>,
  2. <ПараметрыВыполнения>,
  3. <НаборПараметровФункции [НаборПараметровПроцедуры] = Неопределено>.

ИмяФункции [ИмяПроцедуры] – имя функции или процедуры, в которой будет код обработки порции многопоточного выполнения.

ПараметрыВыполнения – структура, параметры многопоточного выполнения. Для облегчения работы можно получить функциями ДлительныеОперации. ПараметрыВыполненияФункции(ИдентификаторФормы) и ДлительныеОперации. ПараметрыВыполненияПроцедуры().

НаборПараметровФункции [НаборПараметровПроцедуры] – соответствие, где ключ произвольное значение, некоторое обобщение данной порции многопоточного выполнения. В общем случае может быть номером порции, или ссылкой на документ, если у нас идет разбивка порций по документам. В значение соответствия помещаем массив параметров функции, описанной в первом параметре. Количество параметров не должно быть больше семи.

Обе функции возвращают одинаковое значение – структуру, в которой описывается результат ее выполнения. Подробнее разберем в статье.

Принцип работы

Перед началом реализации многопоточной работы нужно определится: будем обрабатывать результат или достаточно запустить многопоточное выполнение и забыть про него. В зависимости от этого будем пользоваться ВыполнитьФункциюВНесколькоПотоков в первом случае и ВыполнитьПроцедуруВНесколькоПотоков во втором.

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

Первая функция для подготовки и запуска многопоточного выполнения. В ней необходимо подготовить параметры выполнения и порции для последующего помещения их в функцию ВыполнитьФункциюВНесколькоПотоков [ВыполнитьПроцедуруВНесколькоПотоков].

Вторая функция будет непосредственно обрабатывать порции.

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

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

Работа на клиенте с постобработкой

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

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

Процедура РассчитатьДокументыМногопоточноКлиент(ФормаВладелец, ПериодРасчета) Экспорт
	
	Задание = РасчетДокументовСервер.РассчитатьДокументыМногопоточно(ПериодРасчета);
	Если Задание = Неопределено Тогда
		Возврат;
	КонецЕсли;	
	
	НастройкиОжидания = ДлительныеОперацииКлиент.ПараметрыОжидания(ФормаВладелец);
	НастройкиОжидания.ТекстСообщения = НСтр("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!

Добавить комментарий

Sidebar