Семафоры потоков

03.12.24

База данных - HighLoad оптимизация

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

Бесплатные

ВНИМАНИЕ: Файлы из Базы знаний - это исходный код разработки. Это примеры решения задач, шаблоны, заготовки, "строительные материалы" для учетной системы. Файлы ориентированы на специалистов 1С, которые могут разобраться в коде и оптимизировать программу для запуска в базе данных. Гарантии работоспособности нет. Возврата нет. Технической поддержки нет.

Узнавайте о новых бесплатных решениях в нашей телеграм-группе Инфостарт БЕСПЛАТНО

Наименование Скачано Бесплатно
Семафоры потоков:
.cfe 22,24Kb
38 Скачать бесплатно

Статья написана на основе доклада Методы ускорения группового проведения документов, прочитанного мной на конференции "Infostart tech event 2024", и подробно раскрывает одну его главу: "Параллельно-последовательное проведение". 
 

Предпосылки

 

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


 

Но увы, - так бывает не всегда. Как бы мы ни хотели, нередко идущие подряд документы содержат в себе совпадающую номенклатуру, одинаковые контрагенты или договоры и т.д. Каждый раз в коде проведения нужно выбирать остатки из регистров по одним и тем же измерениям, которые меняются по результатам проведения предыдущих документов.

Так, теряется возможность разделить их в параллель, и приходится проводить снова последовательно.


 

Гибридное проведение

 

Наша идея в том, чтобы только зависимые участки кода проводились последовательно, а остальные участки кода - параллельно.

 

 

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

 

 

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

 

// пример
Процедура ОбработкаПроведения(Отказ, РежимПроведения)

// ... независимый код

// ... начало зависимого кода
    // Таблица взаиморасчетов с учетом зачета авансов
    ТаблицаВзаиморасчеты = УчетВзаиморасчетов.ПодготовитьТаблицуВзаиморасчетовЗачетАвансов(
        ПараметрыПроведения.ЗачетАвансовТаблицаДокумента, ПараметрыПроведения.ЗачетАвансовТаблицаАвансов,
        ПараметрыПроведения.ЗачетАвансовРеквизиты, Отказ);

// ... зависимый код

Движения.Записать();
// ... окончание зависимого кода

// ... снова независимый код

 

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

Таких зависимых участков в коде может быть несколько, и конфликтовать они могут с разными документами внутри одной пачки. И как здесь поступить - есть несколько вариантов. Например, можно каждый такой участок проводить последовательно, либо где возможно - параллельно, где нет – последовательно.

 


 

Координация потоков   


Мы придумали механизм семафоров для координации одновременно запущенных потоков. Наши семафоры могут показывать "красный сигнал" и много "зеленых сигналов" для каждого кусочка зависимого кода в каждом потоке. Зеленый сигнал - это порядковый номер от 1 до N, где N - это общее количество зависимых кусков кода, требующих последовательного выполнения. Каждому такому кусочку кода перед стартом многопоточной обработки присваивается свой порядковый номер, а в работе они опрашивают состояние семафора, ждут совпадения своего номера с показаниями семафора, и только тогда запускаются в работу. По завершении, семафор продвигается на следующее значение, таким образом давая разрешение включаться в работу другому потоку. А текущий же поток одновременно с этим, завершает выполнение кода, следующего после зависимого участка.


 

    Наш механизм семафоров состоит всего из трёх методов и одного регистра:

 

1. ИнициализацияРазрешенияСемафора - создаём Семафор с уникальным идентификатором.

Процедура ИнициализацияРазрешенияСемафора(ИДСемафора) Экспорт
    
    ДатаСоздания = ТекущаяДатаСеанса();
    НаборСемафор = РегистрыСведений.dev_СемафорыПотоков.СоздатьНаборЗаписей();
    НаборСемафор.Отбор.ИД.Установить(ИДСемафора);
    НоваяЗапись = НаборСемафор.Добавить();
    НоваяЗапись.Активность = Истина;
    НоваяЗапись.ИД = ИДСемафора;
    НоваяЗапись.Порядок = 1;
    НоваяЗапись.ДатаСоздания = ДатаСоздания;
    НаборСемафор.Записать(Истина);
    
КонецПроцедуры 

 

2. ОжидатьРазрешенияСемафора - ждём зеленого сигнала (совпадение со своим порядковым номером).

Процедура ОжидатьРазрешенияСемафора(ИДСемафора, ОжидатьПорядок) Экспорт
    
    Текст = "ВЫБРАТЬ
    |    Семафор.Порядок КАК Порядок,
    |    Семафор.ДатаСоздания КАК ДатаСоздания
    |ИЗ
    |    РегистрСведений.dev_СемафорыПотоков КАК Семафор
    |ГДЕ
    |    Семафор.ИД = &ИД
    |
    |ДЛЯ ИЗМЕНЕНИЯ";
    Запрос = Новый Запрос(Текст);
    // пока не вижу смысла ставить блокировки, т.к. хоть и читают все, - пишет всегда только один избранный.
    Запрос.УстановитьПараметр("ИД", ИДСемафора);

    Пока Истина Цикл
        
        НачатьТранзакцию();
            Таблица = Запрос.Выполнить().Выгрузить();
        ЗафиксироватьТранзакцию();
        Если Таблица.Количество() = 0 Тогда
            // вообще нет семаформа. вообщето логическая ошибка. никого больше не ждём
            Возврат;
        КонецЕсли;
        ПерваяСтрока = Таблица[0];
        ТекущийПорядок = ПерваяСтрока.Порядок;
        ДатаСоздания = ПерваяСтрока.ДатаСоздания;
        ТекущаяДата = ТекущаяДатаСеанса();
        ПрошлоВремени = ТекущаяДата - ДатаСоздания;
        Если ПрошлоВремени >= 10 * 60 Или ПрошлоВремени < 0 Тогда
            // Таймаут 10 минут, и выходим.
            Возврат;
        КонецЕсли;
        Если Не(ТипЗнч(ТекущийПорядок) = Тип("Число")) Тогда
            ТекущийПорядок = 0;
        КонецЕсли;
        
        Если ОжидатьПорядок = ТекущийПорядок Тогда
            // подошла наша очередь
            Возврат;
        КонецЕсли;
        
        СколькоПередНами = ОжидатьПорядок - ТекущийПорядок + 1;
        Пауза =  0.1 * СколькоПередНами;
        Dev_СемафорыПотоков.Пауза(Пауза);
        
    КонецЦикла;
    
    // недостижимо. выйдем либо дождавшись, либо по таймауту, либо по логической ошибке,
    // например - удалена запись регистра о семафоре.
    Возврат;
    
КонецПроцедуры 

 

3. ПродвинутьРазрешенияСемафора - сдвигаем семафор на следующий сигнал.

Процедура ПродвинутьРазрешенияСемафора(ИДСемафора, Порядок) Экспорт
    
    Текст = "ВЫБРАТЬ
    |    Семафор.Порядок КАК Порядок,
    |    Семафор.ДатаСоздания КАК ДатаСоздания
    |ИЗ
    |    РегистрСведений.dev_СемафорыПотоков КАК Семафор
    |ГДЕ
    |    Семафор.ИД = &ИД
    |
    |ДЛЯ ИЗМЕНЕНИЯ";
    Запрос = Новый Запрос(Текст);
    // пока не вижу смысла ставить блокировки, т.к. хоть и читают все, - пишет всегда только один избранный.
    Запрос.УстановитьПараметр("ИД", ИДСемафора);
    
    Таблица = Запрос.Выполнить().Выгрузить();
    Если Таблица.Количество() = 0 Тогда
        // вообще нет семаформа - удалили извне
        Возврат;
    КонецЕсли;
    
    ПерваяСтрока = Таблица[0];
    // знаем, что наша очередь. проверять не нужно
    // ТекущийПорядок = ПерваяСтрока.Порядок;
    ДатаСоздания = ПерваяСтрока.ДатаСоздания;
    НаборСемафор = РегистрыСведений.dev_СемафорыПотоков.СоздатьНаборЗаписей();
    НаборСемафор.Отбор.ИД.Установить(ИДСемафора);
    НоваяЗапись = НаборСемафор.Добавить();
    НоваяЗапись.Активность = Истина;
    НоваяЗапись.ИД = ИДСемафора;
    НоваяЗапись.Порядок = Порядок + 1;
    НоваяЗапись.ДатаСоздания = ДатаСоздания;
    НаборСемафор.Записать(Истина);
    
КонецПроцедуры

 

Регистр сведений: Семафоры потоков

 

 ИД – Уникальный идентификатор семафора.


При работе с регистром мы не используем ни транзакции, ни блокировки, т.к. вся работа устроена так, что в один момент времени только один поток может записывать в регистр. Все остальные потоки в этот момент однозначно могут только читать из регистра состояние семафора. Количество таких чтений регистра при 10 потоках на практике доходило до полутора тысяч в секунду. Значение кажется фантастическим («Так быстро не бывает!»), но по факту это эффективная работа внутреннего кэша, не буду вдаваться в подробности, где именно. Но тем не менее, для уменьшения количества обращений в цикле к серверу БД, мы используем паузу, когда известно, что наша очередь ещё не скоро. Чем ближе наша очередь, тем пауза меньше. Эффекта от такого подходы мы не заметили, но это и не важно.

Для вставки механизма в свои конфигурации нужно:

1. Перед запуском пачки документов на многопоточное проведение, определиться с количеством семафоров и зависимыми участками кода. Семафоров может быть несколько. Например, если в пачке из 100 документов попалось всего лишь 3 различных договора, то и семафоров будет 3. Либо: семафор на номенклатуру + семафор на договоры, и т.д. Нужно инициализировать необходимое количество семафоров.

2. Привязать каждый зависимый участок кода к нужному семафору, и назначить порядковые номера в этом семафоре.

3. В начале нужного участка – сделать вставку в код с ожиданием семафора.

4. В конце – сделать вставку с продвижением семафора.


   

Важно: Номера пропускать нельзя! Если в вашем коде возможен досрочный возврат, то желательно понять это на первом этапе, и вообще не назначать семафор. Иначе обязательно пройти ожидание и сдвиг семафора.

Код представлен в прилагаемом расширении конфигурации: общий модуль "dev_СемафорыПотоков", регистр сведений, и обработка "Демонстрация работы семафоров".

Код не привязан ни к какой конкретной конфигурации. Работать может на платформе не ниже 8.3.13. запускали на 8.3.18 и 8.3.24.

 

Демонстрация работы семафоров

 

Обработка из расширения:

- детально показывает, как в реальности происходит координация параллельных потоков на примере гипотетической процедуры обработки проведения по схеме "1+1+1", т.е. 1 секунда - независимый код; далее 1 секунда - зависимый код, который выстраивается строго последовательно строго по порядку; и в завершении ещё 1 секунда независимого кода.

 

 

  • замеряет время начала и завершения зависимого участка кода, выводит всё в табличку, и показывает потери на ожиданиях и переключениях между потоками.
  • можно выбрать количество параллельных потоков, и общее количество документов (гипотетических)
  • можно раскидать документы по потокам как равномерно, так и случайным образом.
  • нетрудно заполнить табличку своими реальными документами (доработав в них код) и посмотреть на результат.

 

 

Нюансы

 

  1. На файловой БД - работать не должна и не будет!
  2. Не выбирайте кол-во потоков больше, чем у вас есть ядер в реальности.
  3. Пару соседних документов в один поток ставить не нужно, т.к. по факту получится их полностью последовательное проведение. Такие примеры можно увидеть, если выбрать случайное раскидывание по потокам.
  4. Соседние документы через одного - также нежелательно ставить в один поток (в нашем примере), т.к. последний будет ждать уже не зеленый семафор, а завершение работы над первым документом.
  5. 5. От трёх потоков, в нашем примере, общее время обработки уже не зависит. В реальности - нужно оценивать соотношение длительности вашего зависимого блока с общей длительностью проведения документа.
  6. 6. Для работы семафора используется запись в регистр сведений. Это минус, потому что идёт обращение к SQL-серверу базы данных как при записи, так и при опросе состояния в цикле.

 

Потери на каждую строку таблицы на практике могут составлять от 15 миллисекунд,  в среднем 170 миллисекунд, а в пике было 260 миллисекунд. Всё зависит от того, чем ещё занят сервер БД кроме наших семафоров, и множества других факторов.

Например, расчетное время проведения 100 документов (гипотетических, по схеме 1+1+1) при количестве потоков от 3 до 10 - составляет 102 секунды (100 зависимых блоков последовательно, + 1 первый в начале и 1 последний в конце).

Реальное время у нас показывает 122-125 секунд. т.е. примерно 20% потери, но результат всё равно выигрышный: если сравнивать с обычным последовательным проведением в 300 секунд, - это в 2.5 раза быстрее.

Вступайте в нашу телеграмм-группу Инфостарт

Семафоры потоков гибридное параллельно последовательное проведение

См. также

HighLoad оптимизация Программист 1C:ERP Бесплатно (free)

Использование оператора «В» для полей или данных составного типа (например, Регистратор) может приводить к неочевидным проблемам.

10.11.2025    5507    ivanov660    48    

51

HighLoad оптимизация Программист 1С:Предприятие 8 1C:ERP Бесплатно (free)

Приведем примеры использования различных в динамических списках и посмотрим, почему это плохо.

18.02.2025    8272    ivanov660    39    

61

HighLoad оптимизация Технологический журнал Системный администратор Программист Бесплатно (free)

Обсудим поиск и разбор причин длительных серверных вызовов CALL, SCALL.

24.06.2024    10675    ivanov660    13    

64

HighLoad оптимизация Программист 1С:Предприятие 8 Бесплатно (free)

Метод очень медленно работает, когда параметр приемник содержит намного меньше свойств, чем источник.

06.06.2024    16681    Evg-Lylyk    73    

46

HighLoad оптимизация Программист 1С:Предприятие 8 1C:Бухгалтерия Бесплатно (free)

Анализ простого плана запроса. Оптимизация нагрузки на ЦП сервера СУБД используя типовые индексы.

13.03.2024    8219    spyke    29    

54

HighLoad оптимизация Программист 1С:Предприятие 8 Бесплатно (free)

Оказывается, в типовых конфигурациях 1С есть, что улучшить!

13.03.2024    11557    vasilev2015    22    

47
Комментарии
Подписаться на ответы Инфостарт бот Сортировка: Древо развёрнутое
Свернуть все
1. gzharkoj 580 04.12.24 09:15 Сейчас в теме
"При работе с регистром мы не используем ни транзакции, ни блокировки," - судя по всему, у вас стоит автоматический режим блокировок, следует из запроса " ДЛЯ ИЗМЕНЕНИЯ", а значит блокировки на уровне СУБД выставляет платформа. Вы семафор построили на блокировках СУБД записи и обновления. Второй момент, блокировки могут ожидать ограниченное время, а у вас по коду нигде нет попыток, что если выполнение будет дольше дефолтных 20 секунд?
Строились бы на управляемых блокировках, меньше бы напрягали СУБД, было бы эффективней. От записей в таблицу не избавились бы, но блокировки-проверки доступности работали бы на сервере 1с в сервисе упр. блокировок.
pbelousov; +1 Ответить
2. pbelousov 47 04.12.24 10:26 Сейчас в теме
(1) вы абсолютно правы, что блокировка есть.
но - не в логике работы семафора!

единственное, зачем она нужна - чтобы не пересечься, например с администратором, который хочет нас убить и чистит регистр в этот момент :)

20 секунд??? нигде в логике нет захвата базы данных на длительное время.
соответственно, и вылетать по таймауту там тоже негде!
проверяли на практике на закрытии месяца 7 часов перепроведение в 10 параллельных потоков.

максимальное время на операцию записи - порядка 300 миллисекунд, и то, когда SQL сервер помимо нас ещё чем-то занят, важными делами. (там ещё под сотню баз данных)

напрягать СУБД меньше не получится, т.к. на запись блокировки СУБД используются в любом случае!

с управляемыми блокировками был ньюанс... а пользы не увидели пока, т.к. от записи в БД не избавились.
думаем... и не только про них.
может быть, ещё расскажу что-нибудь интересное :)
gzharkoj; +1 Ответить
6. starik-2005 3201 06.12.24 10:59 Сейчас в теме
(2) 1.
напрягать СУБД меньше не получится, т.к. на запись блокировки СУБД используются в любом случае!
Лет сто тому назад писал про мьютексы, которые не требуют обращаться к СУБД. Это обычная файловая блокировка. И даже на диске байты не меняются - фактически в памяти весь спинлок происходит.

2.
Паузу же завезли в 24-й или 25-й релиз. Зачем там ваш левый модуль?

3.
Все-равно не понял логику работы такого конвейера.
dlyubanevich; Dach; +2 Ответить
3. DmitryKSL 175 05.12.24 16:58 Сейчас в теме
Интересная идея, ничего подобного не видел.
Т.е. вы переписали "ОбработкаПроведения" во всех документах? Как потом обновлять конфу?
pbelousov; +1 Ответить
4. pbelousov 47 05.12.24 18:32 Сейчас в теме
(3) Спасибо :) это только одна идея из нескольких.

мы сделали вставки только в топовые по времени проведения документы, которые не могли просто так взять и провести параллельно. это Реализация Товаров, и Реализация Отгруженных Товаров.

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

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

да, это боль многих, а мой подход позволяет частично её унять :)

вставок всего две: в начале блока "ОжидатьРазрешенияСемафора" и в завершении блока "ПродвинутьРазрешениеСемафора"

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

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

сейчас у меня на голосовании доклад на предстоящую конференцию - в продолжение этой темы, но там вставок в код уже больше, нужно быть посмелее! поэтому и сложность я в тезисах обозначил как максимальную.
7. starik-2005 3201 06.12.24 11:03 Сейчас в теме
(4)
"мы давно уже проводим параллельно", но что делать, когда во всех документах подряд один товар, один контрагент....
Ну так параллельте запись, а не проведение. На запись тратится львиная доля времени. И если последовательно сформировать таблицы с данными для записи, то потом эти регистры можно в любой последовательности записать. Но да, за историческими данными об остатках партий - или что там у вас есть - придется тоже к какой-то общей таблице бегать, и это даже хорошо, ибо в СУБД лазить за ними не нужно каждый раз. Ну и хранить только то, что поменято ну или будет поменято (вытащить все партии товаров и всех контрагентов за период проведения - это один запрос, и он не в цикле, так что еще и стандарты 1С блюдутся - красота!)
dlyubanevich; Dach; +2 Ответить
5. kasper076 116 06.12.24 10:44 Сейчас в теме
Очень напоминает вот это https://infostart.ru/1c/articles/626117/ но выглядит проще. Возможно потому, что не описан механизм распределения документов по потокам.
8. pbelousov 47 06.12.24 11:40 Сейчас в теме
(5)
моя статья не про то, как запустить многопоточность. Наш опыт об этом я рассказывал в докладе,
и да - ваша ссылка тоже об этом.

а вот когда вы упрётесь в общую боль: "а что делать, когда не получается распараллелить?" ...
вот об этом моя статья.

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

продолжение темы, "когда многопоточка уже бессильна", я надеюсь рассказать вот тут: https://event.infostart.ru/teamlead_2025/agenda/2246594/ по результатам голосования.
11. kasper076 116 06.12.24 14:25 Сейчас в теме
(8) Видимо недостаточно внимательно прочитал статью. Ппоробую еще раз.
9. Dach 390 06.12.24 12:06 Сейчас в теме
(0) идея интересная, конечно, но... выглядит как лютый велосипед с квадратными колесами.

Либо я не понял гениальность идеи, либо...

Вот пример одного из очевидных подводных камней.

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

То есть чем ваш семафорный механизм лучше обычных мьютексов менеджера блокировок - я не понял.

Если Вы сейчас ответите, что в Вашей реализации такое в принципе невозможно и что если кто-то что-то пишет или кто-то что-то ответственно читает, то у него в любом случае до конца транзакции зеленый свет, а у остальных - красный, то тогда еще раз вопрос - а чем это отличается от той же самой обычной блокировки? Можно было просто РС с одним измерением "ИД" создать и по нему накладывать блокировку перед ответственным чтением или записью и все, будет ровно тоже самое и безо всяких семафоров.

Если мы что-то пишем в СУБД или что-то из нее ответственно читаем, то записываемые или ответственно читаемые данные нам в любом случае надо защищать от других сессий до конца транзакции. И ничего другого тут не придумано на текущий момент, кроме блокировок (либо проверки версий строк для таких СУБД как PG и отката транзакции в конце). Так если все так, то напуркуа нам семафоры?

Удивительно просто, еще и на конференцию это попало в виде доклада?

В методе ожидания семафора вообще какой-то странный код:

Пока Истина Цикл
        
        НачатьТранзакцию();
            Таблица = Запрос.Выполнить().Выгрузить();
        ЗафиксироватьТранзакцию();


"Пока Истина" - ну это пять )))
И зачем запрос в транзакции выполнять, если нет наложения блокировки перед ним? У вас что, база в автоматическом режиме управления блокировками работает?
dlyubanevich; +1 Ответить
10. pbelousov 47 06.12.24 13:30 Сейчас в теме
(9)
В итоге держим эти блокировки хз сколько.


транзакции нигде не держатся!
уже отвечал комментаторам. повнимательней пожалуйста.

реализация проста, но надёжна! сам факт записи в БД, это конечно "минус", но вариантов не так много.

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

а то что тут чуть раньше предложили на файловых блокировках - там комментировать, только портить.
в плане надежности ноль.

за пятёрку спасибо! блокировки есть (опять невнимательность) но, как уже отвечал выше - не в логике работы с семафорами.
12. user700522_lerner584 11.12.24 07:41 Сейчас в теме
По оформлению статьи: говориться везде про семафор, а нарисован светофор-)
user1523931; artbear; rpgshnik; +3 Ответить
13. pbelousov 47 11.12.24 09:19 Сейчас в теме
Для отправки сообщения требуется регистрация/авторизация