Почему ПолучитьИзВременногоХранилища возвращает Неопределено в фоновом задании и как правильно отображать прогресс выполнения?

Программист 1С v8.3 (Управляемые формы) 1С:Управление нашей фирмой IT и автоматизация бизнеса
← На главную

При разработке интерактивных инструментов в среде «1С:Предприятие» мы часто сталкиваемся с необходимостью вынесения тяжелых расчетов в фоновые задания. Это позволяет не блокировать интерфейс пользователя и сохранять отзывчивость приложения. Однако при попытке передать текущий статус выполнения или промежуточные расчеты обратно на форму «на лету» разработчики часто совершают классическую ошибку: пытаются использовать для этого временное хранилище. В результате функция ПолучитьИзВременногоХранилища упорно возвращает Неопределено до тех пор, пока фоновое задание полностью не завершится.

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

Почему временное хранилище возвращает «Неопределено» во время работы задания?

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

Когда вы запускаете фоновое задание, платформа «1С:Предприятие» создает для него отдельный фоновый сеанс. Этот сеанс физически не имеет прямого доступа к временному хранилищу вашего основного (интерактивного) сеанса, в котором открыта форма. Единственным исключением из этого правила является передача данных по завершении фонового задания, которая работает по строгому регламенту:

  1. В родительском (клиентском) сеансе генерируется уникальный адрес во временном хранилище с привязкой к уникальному идентификатору формы: АдресХранилища = ПоместитьВоВременноеХранилище(Неопределено, УникальныйИдентификатор).
  2. Этот адрес передается в фоновое задание в качестве одного из параметров запуска.
  3. Фоновое задание в процессе своей работы выполняет запись результатов по этому адресу: ПоместитьВоВременноеХранилище(РезультатВыполнения, АдресХранилища).
  4. Критический момент: физический перенос записанных данных из временного хранилища фонового сеанса в хранилище родительского сеанса происходит только в момент успешного завершения фонового задания. Пока фоновое задание активно выполняется (имеет статус «Активно»), любая попытка прочитать данные по этому адресу из основного сеанса вернет Неопределено.

Именно поэтому попытка использовать связку ПоместитьВоВременноеХранилище и ПолучитьИзВременногоХранилища для динамического обновления прогресс-бара во время работы фонового процесса обречена на неудачу.

Как работает штатный механизм передачи прогресса в БСП

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

Давайте разберем по шагам, как это работает:

Пошаговое руководство по исправлению кода

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

Шаг 1. Переписываем серверный метод фонового задания

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


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

Шаг 2. Реализуем запуск и ожидание на клиенте

Теперь настроим клиентскую часть нашей внешней обработки. Вместо ручного написания обработчиков ожидания и разбора статусов, мы задействуем штатный асинхронный механизм БСП — метод ДлительныеОперацииКлиент.ОжидатьЗавершение. Этот метод берет на себя всю рутину по опросу сервера, визуализации прогресс-бара и корректному завершению ожидания.

Добавим в модуль управляемой формы нашей обработки следующий код:


&НаКлиенте
Процедура ОбновитьОстаток(Команда)

    // Подготавливаем параметры для фонового задания
    ПараметрыВыполнения = ПолучитьПараметрыВыполненияНаСервере();
    
    // Запускаем фоновое задание на сервере
    ДлительнаяОперация = НачатьВыполнениеНаСервере(ПараметрыВыполнения);
    
    Если ДлительнаяОперация.Статус = "Выполнено" Тогда
        // Если операция выполнилась мгновенно (на малом объеме данных)
        ПослеЗавершенияОперации(ДлительнаяОперация, Неопределено);
    Иначе
        // Настраиваем параметры ожидания
        ПараметрыОжидания = ДлительныеОперацииКлиент.ПараметрыОжидания(ЭтотОбъект);
        ПараметрыОжидания.ВыводитьПрогрессВыполнения = Истина;
        ПараметрыОжидания.ВыводитьСообщения = Истина;
        ПараметрыОжидания.ТекстСообщения = НСтр("ru = 'Выполняется обновление остатков номенклатуры...'");
        
        // Описание оповещения, которое сработает после завершения фонового задания
        ОповещениеОЗавершении = Новый ОписаниеОповещения("ПослеЗавершенияОперации", ЭтотОбъект);
        
        // Запускаем стандартный интерактивный цикл ожидания БСП
        ДлительныеОперацииКлиент.ОжидатьЗавершение(ДлительнаяОперация, ОповещениеОЗавершении, ПараметрыОжидания);
    КонецЕсли;

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

&НаСервереБезКонтекста
Функция НачатьВыполнениеНаСервере(Знач ПараметрыВыполнения)
    
    // Регистрируем параметры запуска длительной операции
    НастройкиЗапуска = ДлительныеОперации.ПараметрыВыполненияВФоне(Новый УникальныйИдентификатор);
    НастройкиЗапуска.НаименованиеФоновогоЗадания = НСтр("ru = 'Обновление остатков номенклатуры'");
    
    // Имя метода, который будет запущен в фоне (должен быть в экспортном модуле)
    ИмяМетода = "УправлениеНебольшойФирмойСервер.ОбновитьОстатокВНоменклатуре";
    
    Возврат ДлительныеОперации.ВыполнитьВФоне(ИмяМетода, ПараметрыВыполнения, НастройкиЗапуска);
    
КонецФункции

&НаСервере
Функция ПолучитьПараметрыВыполненияНаСервере()
    
    ПараметрыВыполнения = Новый Структура;
    ПараметрыВыполнения.Вставить("Запасы", Объект.Запасы.Выгрузить());
    Возврат ПараметрыВыполнения;
    
КонецФункции

&НаКлиенте
Процедура ПослеЗавершенияОперации(РезультатОперации, ДополнительныеПараметры) Экспорт
    
    Если РезультатОперации.Статус = "Выполнено" Тогда
        
        // Теперь задание выполнено, и мы МОЖЕМ безопасно прочитать финальные данные из временного хранилища
        РезультатВычислений = ПолучитьИзВременногоХранилища(РезультатОперации.АдресХранилища);
        ПоказатьПредупреждение(, НСтр("ru = 'Обработка данных успешно завершена!'"));
        
    ИначеЕсли РезультатОперации.Статус = "Отменено" Тогда
        ПоказатьПредупреждение(, НСтр("ru = 'Операция была отменена пользователем.'"));
    Иначе
        // В случае ошибки выводим подробный текст исключения
        ВызватьИсключение РезультатОперации.КраткоеПредставлениеОшибки;
    КонецЕсли;
    
КонецПроцедуры

Важные правила оптимизации прогресса в БСП

Для того чтобы ваш код работал быстро и не создавал избыточной нагрузки на сервер СУБД и рабочие процессы платформы, придерживайтесь следующих рекомендаций:

← На главную