Как реализовать мьютекс и предотвратить параллельное выполнение кода в 1С

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

В практике разработки на платформе 1С:Предприятие 8 часто возникает задача синхронизации потоков. Например, когда необходимо гарантировать, что регламентное задание по отправке почты не запустится одновременно с ручным процессом рассылки, или когда несколько сеансов не должны одновременно обрабатывать одни и те же записи регистра. В программировании для этих целей используется мьютекс (mutex — mutual exclusion). Разберем наиболее эффективные и простые способы реализации этого механизма средствами платформы.

Способ 1. Использование ключа фонового задания

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

Рассмотрим пример реализации:


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

Выясним причину эффективности этого метода: ФоновыеЗадания на уровне кластера гарантируют, что в активном состоянии может находиться только одно задание с конкретным именем метода и ключом. Это избавляет нас от необходимости создавать дополнительные объекты в базе данных.

Способ 2. Объектная блокировка на служебном справочнике

Если задача стоит шире и нужно заблокировать выполнение участка кода в обычном сеансе (не обязательно в фоновом), можно воспользоваться механизмом объектных блокировок. Рассмотрим по шагам этот алгоритм:

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

Посмотрим на пример кода:


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

Важный момент: метод Заблокировать() устанавливает объектную блокировку, которая действует до тех пор, пока объект не будет уничтожен в памяти или не будет вызван метод Разблокировать(). Однако стоит помнить, что этот метод чаще используется в "толстом" клиенте или при явном управлении объектами.

Способ 3. Управляемые блокировки (DataLock)

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

Разберем пример с использованием объекта БлокировкаДанных:


ЭлементБлокировки = БлокировкаДанных.Добавить("РегистрСведений.ОчередьПисем");
ЭлементБлокировки.УстановитьЗначение("Обработано", Ложь);
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;

НачатьТранзакцию();
Попытка
    БлокировкаДанных.Заблокировать();
    // Читаем и обрабатываем данные
    // Другие сеансы, пытающиеся наложить такую же блокировку, будут ждать
    ЗафиксироватьТранзакцию();
Исключение
    ОтменитьТранзакцию();
    ВызватьИсключение;
КонецПопытки;

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

Способ 4. Атомарное изменение статуса в запросе

Если ваша задача — обработка очереди (как в случае с письмами), можно обойтись без классического мьютекса, применив стратегию "захвата" записей. Рассмотрим этот метод подробнее. Вместо того чтобы блокировать весь регистр, мы помечаем записи как "взятые в работу".

Для этого в запросе используем конструкцию ДЛЯ ИЗМЕНЕНИЯ:


НачатьТранзакцию();

Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ ПЕРВЫЕ 10
    |   Очередь.Ссылка КАК Ссылка
    |ИЗ
    |   РегистрСведений.ОчередьПисем КАК Очередь
    |ГДЕ
    |   Очередь.Статус = ЗНАЧЕНИЕ(Перечисление.СтатусыПисем.ГотовоКОтправке)
    |ДЛЯ ИЗМЕНЕНИЯ";

Результат = Запрос.Выполнить();
// Сразу меняем статус на "В процессе", чтобы другие потоки их не увидели
Выборка = Результат.Выбрать();
Пока Выборка.Следующий() Цикл
    МенеджерЗаписи = РегистрыСведений.ОчередьПисем.СоздатьМенеджерЗаписи();
    // ... заполнение ключевых полей ...
    МенеджерЗаписи.Статус = Перечисления.СтатусыПисем.ВПроцессе;
    МенеджерЗаписи.Записать();
КонецЦикла;

ЗафиксироватьТранзакцию();
// Теперь спокойно отправляем письма из выбранного списка

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

Дополнительные рекомендации

При выборе способа стоит учитывать архитектуру вашего решения:

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

← На главную