Почему процедуры в обычных формах 1С запускаются «параллельно» и как предотвратить конфликты при реентерабельности?

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

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

Суть проблемы: когда «однопоточная» 1С становится «многопоточной»

Проанализируем классическую ситуацию: у нас есть форма обработки, в которой описана процедура для печати документов. Внутри процедуры организован цикл, а чтобы принтер успевал обрабатывать задания, добавлена небольшая пауза. Часто для реализации паузы в 1С используют внешние средства, например, объект WScript.Shell и команду ping. Казалось бы, пока работает код, форма должна быть заблокирована, и пользователь не может вызвать выполнение того же действия снова.

Однако, если во время этой «паузы» пользователь совершает действие (например, сканирует штрихкод, который инициирует событие или нажимает «горячую клавишу»), платформа 1С внезапно прерывает ожидание текущей процедуры и запускает новый экземпляр той же самой процедуры. В итоге мы получаем два (или более) активных выполнения одного и того же кода в рамках одного сеанса. Разберем, почему это происходит с точки зрения архитектуры Windows и 1С.

Разбираем механизм возникновения реентерабельности

Причина кроется в том, как операционная система Windows и платформа 1С обрабатывают очередь сообщений (Message Queue). 1С — это событийное приложение. Когда мы вызываем внешний COM-объект, такой как WScript.Shell, и просим его выполнить команду с ожиданием, поток управления на мгновение может «ослабить хватку» над основным циклом событий.

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

Важно понимать: это не честная многопоточность, где задачи выполняются параллельно на разных ядрах процессора. Это именно вытеснение в стеке. Процедура №1 «замирает», выполняется Процедура №2 до самого конца, и только после этого Процедура №1 продолжает работу с той же строки, где она остановилась.

Опасность общих данных и «состояние гонки»

Самая большая проблема заключается в использовании переменных модуля или реквизитов обработки. Рассмотрим пример: в модуле объявлена переменная ТекстОшибки. Процедура №1 записывает туда данные, уходит на «паузу». В этот момент вклинивается Процедура №2, которая также использует ТекстОшибки для своих нужд и меняет её значение. Когда Процедура №1 просыпается, она читает значение ТекстОшибки и видит там данные, которые туда записала Процедура №2. Это классическое «состояние гонки» (Race Condition).

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


Перем ТекстОшибки; // Переменная модуля

Процедура ВыполнитьПечать()
    ТекстОшибки = "";
    Для Каждого Док Из СписокДокументов Цикл
        Попытка
            // Печать документа
            Док.ПолучитьОбъект().Напечатать();
        Исключение
            ТекстОшибки = "Ошибка печати: " + ОписаниеОшибки();
        КонецПопытки;
        
        // Коварная пауза, открывающая окно для реентерабельности
        Shell = Новый COMОбъект("WScript.Shell");
        Shell.Run("ping -n 1 -w 2000 127.0.0.1", 0, Истина);
    КонецЦикла;

    Если ЗначениеЗаполнено(ТекстОшибки) Тогда
        Сообщить(ТекстОшибки); // Здесь может оказаться ошибка от "параллельного" вызова!
    КонецЕсли;
КонецПроцедуры

Методы решения: защита кода от повторного входа

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

1. Использование флага занятости (Мьютекс на уровне модуля)

Это самый простой и эффективный способ для обычных форм. Мы создаем переменную, которая будет хранить состояние «выполняется ли сейчас процедура». Если кто-то пытается запустить процедуру, пока флаг установлен, мы просто выходим из неё или выдаем предупреждение.

Разберем реализацию по шагам:

  1. Объявляем переменную в разделе Перем модуля.
  2. В самом начале процедуры проверяем её значение.
  3. Если занято — прерываем выполнение.
  4. Если свободно — устанавливаем флаг и обязательно используем конструкцию Попытка...КонецПопытки, чтобы сбросить флаг даже в случае критической ошибки.

Посмотрим на пример правильной реализации:


Перем ОбработкаИдет; // Флаг состояния

Процедура ВыполнитьПечать()
    
    // Проверяем, не запущена ли уже эта процедура
    Если ОбработкаИдет = Истина Тогда
        Возврат; // Просто игнорируем повторный вызов
    КонецЕсли;
    
    ОбработкаИдет = Истина; // Блокируем вход
    
    Попытка
        // Основной алгоритм
        Для Каждого Док Из СписокДокументов Цикл
            // ... код печати ...
            // Пауза
            Shell = Новый COMОбъект("WScript.Shell");
            Shell.Run("ping -n 1 -w 2000 127.0.0.1", 0, Истина);
        КонецЦикла;
    Исключение
        // Важно сбросить флаг, даже если произошла ошибка
        ОбработкаИдет = Ложь;
        ВызватьИсключение; 
    КонецПопытки;
    
    ОбработкаИдет = Ложь; // Освобождаем "замок"
    
КонецПроцедуры

// Инициализация при открытии
ОбработкаИдет = Ложь;

2. Отказ от небезопасных пауз

Выясним причину, почему ping через WScript.Shell так опасен. Параметр Истина в методе Run заставляет 1С ждать завершения процесса, но само ожидание в реализации COM-объекта может быть выполнено так, что оно не блокирует системную очередь сообщений приложения.

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

3. Использование локальных переменных вместо переменных модуля

Чтобы избежать порчи данных при повторном входе, старайтесь минимизировать использование Перем в шапке модуля и реквизитов объекта для хранения промежуточных результатов. Если данные хранятся внутри процедуры (локальные переменные), то при каждом новом входе в процедуру для неё будет создаваться свой собственный набор локальных переменных в стеке.

Проанализируем разницу: если ТекстОшибки объявлен внутри процедуры ВыполнитьПечать, то Процедура №2 создаст свою копию этой переменной, и когда Процедура №1 вернется к работе, она обратится к своей собственной (старой) версии переменной, не затронутой вторым вызовом.

Почему 1С не сделала полноценную многопоточность в клиенте?

Многих разработчиков интересует вопрос: если система фактически позволяет запускать процедуры «параллельно» через реентерабельность, почему бы не сделать это штатной фичей?

Ответ кроется в безопасности данных. Полноценная многопоточность требует реализации механизмов Thread Safety (потокобезопасности). Ядро 1С, объекты метаданных и глобальный контекст не рассчитаны на одновременное изменение из разных потоков одного сеанса. Это привело бы к повреждению памяти и непредсказуемым падениям платформы. Именно поэтому в платформе 8.3 был выбран путь изоляции — выполнение тяжелых задач выносится в фоновые задания, которые работают в своем собственном адресном пространстве.

Подведем итог: если вы работаете с обычными формами и используете внешние вызовы (COM, ВК, Shell), всегда помните о риске реентерабельности. Использование простого логического флага ОбработкаИдет защитит вашу систему от дублирования транзакций и порчи переменных, обеспечив стабильную работу кода даже при активных действиях пользователя.

← На главную