Клонируем обновлятор для выполнения операций в несколько потоков

  оглавление  Многопоточное выполнение операций над базами 1с

Использование COM-объектов в OneScript

Введение

Проблема корректного создания и освобождения COM-объектов в любом managed языке (со сборщиком мусора) сложна и многогранна - столько всего уже написано на эту тему и всё равно возникают постоянные споры и недопонимания на форумах.

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

Я лишь опишу свой опыт применительно к использованию OneScript для общения с базами 1С через внешнее соединение при запуске из Обновлятора (хотя способ запуска на самом деле не имеет значения).

При этом я не буду останавливаться на самом понятии COM-объекта (в этом смысле я всех отсылаю к замечательной книге "Основы COM" Дейла Роджерсона).

Также я не буду останавливаться на том, как COM-объекты уживаются в языках с автоматическим управлением памятью, к которым относится в том числе OneScript.

В этой статье будут лишь практические выводы.

Суть проблемы

А проблема состоит в том, что при выполнении кода через внешнее соединение с базой (которое само по себе является COM-объектом) порождается большое количество как явных (которые мы сами объявили), так и неявных COM-объектов.

И если мы не уничтожаем эти объекты напрямую, то они уничтожаются автоматически в порядке и в момент, когда это сочтёт нужным сделать среда выполнения.

В целом в идеальном мире это не должно быть проблемой и COM-библиотеки должны учитывать этот момент. И если бы это было всегда так - мне не пришлось бы вообще писать эту статью.

К сожалению, практика в целом и применительно к COM-библиотеке для внешнего подключения к базам 1С в частности показывает, что порядок уничтожения всех COM-объектов должен быть задан явно и он должен быть обратным порядку их создания.

И если этого не делать, то наш скрипт будет отлично работать на одних компьютерах (или с одной платформой 1с) и при этом валиться с ошибкой на других компьютерах (других платформах 1с).

Ошибка будет возникать в самом конце работы скрипта при уничтожении COM-объектов сборщиком мусора. Такая ошибка будет нестабильной и в лучшем случае будет просто приводить к тому, что не будет корректно завершаться соединение с базой. То есть скрипт уже отработает, а консоль сервера 1с будет показывать, что соединение с базой ещё есть.

При этом сам скрипт отработает замечательно и выполнит всё, что мы от него хотим, но вот само соединение с базой будет завершено некорректно и код ошибки от OneScript чаще всего будет -1073741819.

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

Первый пример

Рассмотрим простейший скрипт по выводу списка пользователей:

	СписокПользователей = v8.ПользователиИнформационнойБазы.ПолучитьПользователей();
 
	Сообщить("Выводим всех пользователей базы:");
 
	Для Каждого Пользователь Из СписокПользователей Цикл
		Сообщить(Пользователь.Имя);
	КонецЦикла;

Какие здесь COM-объекты мы видим:

  1. v8 - этот объект был создан обновлятором явно и уничтожается он в процедуре ПриОкончанииРаботы.
  2. v8.ПользователиИнформационнойБазы - здесь мы обратились через точку к менеджеру пользователей информационной базы и новый COM-объект был создан неявно средой выполнения OneScript. Это недопустимая для нас ситуация, так как мы не сможем освободить такой объект в нужный нам момент. Ниже я покажу как избавиться от такого неявного создания объекта.
  3. СписокПользователей - этот COM-объект нам вернул метод ПолучитьПользователей.
  4. Пользователь - этот COM-объект создаётся на каждой итерации цикла.

Вроде бы всё? А вот и нет. Здесь присутствует ещё один неявно создаваемый COM-объект внутри среды выполнения. И причина его создания - использование цикла Для Каждого. При использовании такого цикла создаётся итератор для СписокПользователей и этот итератор содержит внутренний COM-объект, который мы также не сможем освободить. Отсюда сразу правило - следует избегать циклов Для Каждого при обходе COM-коллекций.

А вот как следует переписать этот код, чтобы после его выполнения были явно и в нужном порядке освобождены все созданные в нём COM-объекты:

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

Обратите внимание, что здесь мы:

  1. Сохранили обращение к менеджеру информационных баз в отдельную переменную, чтобы затем явно вызвать его освобождение.
  2. Избавились от цикла Для Каждого.
  3. На каждом шаге цикла освобождаем объект Пользователь.
  4. Обернули весь код в блок Попытка Исключение, чтобы после его выполнения (целиком или частично в случае ошибок) гарантированно освободить все созданные COM-объекты. При этом мы опустили обработку ошибок (ничего не написали внутри блока Исключение КонецПопытки).

Второй пример

Предположим, что мы программно создаём обработку из конфигурации базы, чтобы запустить её выполнение из нашего кода.

Код создания обработки будет таким:

МодульЗагрузки = v8.Обработки.ИмпортКейса.Создать()

Здесь:

  1. Неявно создаётся COM-объект v8.Обработки
  2. Неявно создаётся COM-объект v8.Обработки.ИмпортКейса
  3. Явно создаётся COM-объект обработки и сохраняется в переменной МодульЗагрузки.

При таком коде мы сможем явно освободить только МодульЗагрузки, а вот с двумя неявно созданными COM-объектами мы ничего поделать не сможем.

Поэтому такой код должен быть переписан вот так:

Обработки = Неопределено;
ИмпортКейса = Неопределено;
МодульЗагрузки = Неопределено;
 
Попытка
	Обработки = v8.Обработки;
	ИмпортКейса = Обработки.ИмпортКейса;
	МодульЗагрузки = ИмпортКейса.Создать();
 
	// остальной код...
Исключение
КонецПопытки;
 
Если МодульЗагрузки <> Неопределено Тогда
	ОсвободитьОбъект(МодульЗагрузки);
КонецЕсли;
 
Если ИмпортКейса <> Неопределено Тогда
	ОсвободитьОбъект(ИмпортКейса);
КонецЕсли;
 
Если Обработки <> Неопределено Тогда
	ОсвободитьОбъект(Обработки);
КонецЕсли;

Третий пример

А что будет, если мы в нашем скрипте выполним вот такой код (выдержка из предыдущего примера):

	Обработки = v8.Обработки;
	ИмпортКейса = Обработки.ИмпортКейса;
	ИмпортКейса.Создать();

Обратите внимание на то, что мы вызвали метод Создать(), который вернул нам COM-объект, но мы его никуда не сохранили.

Такой код будет ошибкой, так как если метод возвращает COM-объект, то этот объект остаётся висеть в памяти, даже если мы его не сохранили и не работаем с ним в коде.

Да, в этом случае такой код не имел бы смысла (зачем создавать экземпляр обработки и не использовать его), но могут быть ситуации, когда мы вызываем некоторый метод у COM-объекта и не обрабатываем результат этого метода, так как он нам не важен. И вот если в этой ситуации окажется, что результат метода тоже COM-объект, который мы не сохранили и соотв. не освободили явно - нас ждут проблемы.

Большой пример скрипта

В качестве реального примера скрипта, который написан по всем правилам освобождения COM-объектов я предлагаю рассмотреть код загрузки комплектов отчётности в формате Repx. Его можно найти на github.

И это всё?

К сожалению, не всё - бывают более сложные ситуации связанные с подсчётом ссылок на COM-объекты, которые могут приводить к проблемам.

А можно не заморачиваться?

Я согласен, что написание реального кода, в котором явно и в нужном порядке освобождаются все COM-объекты задача не из лёгких, так как способов "выстрелить себе в ногу" при этом предостаточно.

Вы можете писать код точно также, как будто он выполняется прямо в базе, и игнорировать код ошибки, который вам возвращает OneScript.

При такой стратегии я рекомендую процедуру ПриОкончанииРаботы переписать с достаточно большой паузой в конце - как показывает практика это несколько повышает шансы на то, что оставшиеся в памяти COM-объекты будут завершены без ошибок.

Вот этот код:

Процедура ПриОкончанииРаботы()
 
	Если v8 <> Неопределено Тогда
		Попытка
			ОсвободитьОбъект(v8);
			v8 = Неопределено;
		Исключение
		КонецПопытки;
	КонецЕсли;
 
	Если connector <> Неопределено Тогда
		Попытка
			ОсвободитьОбъект(connector);
			connector = Неопределено;
		Исключение
		КонецПопытки;
	КонецЕсли;
 
	Если updater <> Неопределено Тогда
		Попытка
			ОсвободитьОбъект(updater);
			updater = Неопределено;
		Исключение
		КонецПопытки;
	КонецЕсли;
 
	// Ожидание в конце выполнения программы
	// магическим образом помогает избежать
	// проблем с освобождением ресурсов, если
	// мы использовали внешнее подключение к
	// базе.
	Приостановить(10000); // 10 секунд
 
	Если errors Тогда
		ЗавершитьРаботу(1);
	КонецЕсли;
 
КонецПроцедуры

А есть ли альтернатива?

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

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

Для этого нужно написать обработку, которая при своём открытии в базе сразу выполняет нужный код, а затем закрывает базу.

Запустить такую обработку можно через пакетный скрипт, используя шаблон "Открытие базы и внешний запуск обработки":

%run_1c_e% /Execute "c:\Report.epf" :: открытие базы и запуск внешней обработки

Тут главные проблемы:

  1. Отсутствие обратной связи от обработки
  2. Может оказаться так, что при запуске базы появится какое-нибудь служебное окно, которое будет препятствовать выполнению обработки.
С уважением, (преподаватель школы 1С программистов и разработчик обновлятора).



Владимир Милькин
Как помочь сайту: расскажите (кнопки поделиться ниже) о нём своим друзьям и коллегам. Сделайте это один раз и вы внесете существенный вклад в развитие сайта. На сайте нет рекламы, но чем больше людей им пользуются, тем больше сил у меня для его поддержки.

Нажмите одну из кнопок, чтобы поделиться:



Клонируем обновлятор для выполнения операций в несколько потоков

  оглавление  Многопоточное выполнение операций над базами 1с