Как намеренно вызвать взаимную блокировку (Deadlock) в 1С для тестирования

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

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

Понятие взаимной блокировки и отличие от таймаута

Проанализируем механику процесса. Когда сеанс №1 блокирует ресурс «А» и пытается получить доступ к ресурсу «Б», а сеанс №2 в это же время уже заблокировал «Б» и пытается получить доступ к «А», возникает циклическая зависимость. Система (менеджер блокировок 1С или СУБД) понимает, что ни один из потоков не сможет продолжить работу, и принудительно прерывает одну из транзакций, выбирая её в качестве «жертвы» (deadlock victim). В этот момент мы получаем ошибку TDEADLOCK.

Способ №1: Классическая перекрестная блокировка (A-B / B-A)

Рассмотрим самый наглядный метод, основанный на разном порядке обращения к объектам метаданных. Для этого нам понадобятся два сеанса 1С и два объекта (например, два документа или две разные записи в регистре сведений).

Разберем по шагам алгоритм действий:

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

В этот момент менеджер блокировок обнаруживает цикл и мгновенно «убивает» одну из транзакций. Посмотрим на пример кода, который можно использовать в обработке (в отладке также поможет специальный помощник):


// Пример для двух сеансов с разным порядком вызова
Процедура ВыполнитьБлокировку(ПервыйОбъект, ВторойОбъект)
    Блокировка = Новый БлокировкаДанных;
    
    // Блокируем первый ресурс
    Элемент = Блокировка.Добавить("Справочник.Номенклатура");
    Элемент.УстановитьЗначение("Ссылка", ПервыйОбъект);
    Элемент.Режим = РежимБлокировкиДанных.Исключительный;
    
    НачатьТранзакцию();
    Блокировка.Заблокировать();
    
    // Здесь делаем паузу для переключения между сеансами
    Предупреждение("Ресурс 1 заблокирован. Нажмите ОК для попытки блокировки Ресурса 2", 60);
    
    // Пытаемся заблокировать второй ресурс
    Элемент2 = Блокировка.Добавить("Справочник.Номенклатура");
    Элемент2.УстановитьЗначение("Ссылка", ВторойОбъект);
    Элемент2.Режим = РежимБлокировкиДанных.Исключительный;
    
    Блокировка.Заблокировать(); // Здесь возникнет Deadlock, если второй сеанс сделал наоборот
    ЗафиксироватьТранзакцию();
КонецПроцедуры

Способ №2: Эскалация блокировки (Shared -> Exclusive)

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

Рассмотрим ситуацию подробнее:

  1. Сеанс №1 устанавливает РежимБлокировкиДанных.Разделяемый на запись регистра.
  2. Сеанс №2 устанавливает такой же Разделяемый режим на эту же самую запись. Это разрешено, так как разделяемые блокировки не конфликтуют друг с другом.
  3. Сеанс №1 пытается выполнить запись (или явно вызвать Исключительный режим). Он встает в очередь, ожидая, когда Сеанс №2 отпустит свою разделяемую блокировку.
  4. Сеанс №2 также пытается выполнить запись. Он встает в очередь, ожидая Сеанс №1.

Система видит, что оба участника ждут друг друга для повышения уровня владения ресурсом, и фиксирует взаимную блокировку. Пример кода для эмуляции этого сценария:


Процедура ЭмуляцияПовышенияБлокировки()
    Блокировка = Новый БлокировкаДанных;
    Элемент = Блокировка.Добавить("РегистрСведений.ЦеныНоменклатуры");
    Элемент.УстановитьЗначение("Номенклатура", Справочники.Номенклатура.НайтиПоКоду("001"));
    
    НачатьТранзакцию();
    
    // Шаг 1: Оба сеанса должны дойти сюда и выполнить это
    Элемент.Режим = РежимБлокировкиДанных.Разделяемый;
    Блокировка.Заблокировать();
    
    // Пауза для синхронизации сеансов
    Приостановить(10000); // Имитация задержки
    
    // Шаг 2: Оба сеанса пытаются "повысить" блокировку до исключительной
    Элемент.Режим = РежимБлокировкиДанных.Исключительный;
    Блокировка.Заблокировать(); // Менеджер блокировок вызовет TDEADLOCK
    
    ЗафиксироватьТранзакцию();
КонецПроцедуры

Почему блокировки могут «исчезать» при включении отладки?

Проанализируем интересное явление: иногда при включении детального сбора данных в Технологическом журнале (события SDBL, TLOCK) воспроизвести Deadlock становится сложнее. Это происходит из-за того, что запись логов вносит микроскопические задержки в работу сервера приложений (особенно если требуется убирать лишние переносы строк в ТЖ). Этих долей секунды может быть достаточно, чтобы один из сеансов успел завершить операцию чуть раньше другого, превращая взаимную блокировку в обычное ожидание (таймаут). Это стоит учитывать при настройке диагностических фильтров.

Как правильно диагностировать проблему

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

В тексте события TDEADLOCK платформа обычно указывает структуру WaitConnections. Это список номеров сеансов, которые участвовали в конфликте. Если вы используете MS SQL Server, дополнительную информацию можно получить через Deadlock Graph в профайлере SQL, который покажет конфликт на уровне страниц базы данных (например, если индексы неоптимальны и происходит блокировка диапазона Range Lock).

Рекомендации по предотвращению

Чтобы в реальной работе не сталкиваться с тем, что мы только что эмулировали, придерживайтесь следующих правил:

  1. Соблюдайте строгий порядок: Всегда блокируйте и записывайте объекты в одинаковой последовательности во всех модулях системы.
  2. Избегайте повышения уровня: Если вы знаете, что данные будут изменены, сразу устанавливайте Исключительный режим блокировки при чтении. Не используйте сначала разделяемую, а потом исключительную блокировку в одной транзакции.
  3. Минимизируйте транзакции: Старайтесь не выполнять длительные расчеты или внешние запросы (HTTP/Web-сервисы) внутри открытой транзакции, так как это увеличивает время удержания ресурсов.
← На главную