Использование префикса LOCK

Тема в разделе "WASM.ASSEMBLER", создана пользователем Entropy, 5 апр 2026.

  1. Entropy

    Entropy Member

    Публикаций:
    0
    Регистрация:
    23 авг 2020
    Сообщения:
    267
    Если в программе работает много потоков,они могут обращаться к одной и той же области памяти,можно этими потоками управлять одним только префиксом LOCK или лучше воспользоваться средствами WinAPI ?
    Код (ASM):
    1.  
    2. include masm64rt.inc
    3.  
    4.     includelib kernel32.lib
    5.     includelib user32.lib
    6.     includelib shell32.lib
    7.     includelib gdiplus.lib
    8.     includelib gdi32.lib
    9.     includelib msvcrt.lib
    10.  
    11.  
    12.  
    13.     .data
    14.  
    15.  
    16.    locked_var byte ?
    17.  
    18.  
    19.  
    20.     .code
    21.  
    22. entry_point proc
    23.  
    24.     LOCAL hProcess :QWORD
    25.  
    26.  
    27.     mov hProcess, rv(GetCurrentProcess)
    28.  
    29.     invoke SetPriorityClass,hProcess,HIGH_PRIORITY_CLASS
    30.  
    31.     invoke CreateThread,0,0,ADDR threadproc,0,0,0
    32.  
    33.  
    34.  
    35. potential_crash:
    36.  
    37. dec [locked_var]
    38.  
    39. jmp potential_crash
    40.  
    41.  
    42.  
    43.     ret
    44.  
    45. entry_point endp
    46.  
    47.  
    48.  
    49.  
    50. threadproc proc arg:QWORD
    51.  
    52.  
    53. increment:
    54.  
    55. lock inc [locked_var]
    56.  
    57. cmp [locked_var],200
    58. je reset
    59.  
    60. jmp increment
    61.  
    62. reset:
    63. mov [locked_var],0
    64. jmp increment
    65.  
    66.     ret
    67.  
    68. threadproc endp
    69.  
    70.  
    71.     end
    72.  
     
  2. aa_dav

    aa_dav Active Member

    Публикаций:
    0
    Регистрация:
    24 дек 2008
    Сообщения:
    590
    От задачи зависит.
    Если, например, 100 потоков просто должны отчитаться что они завершили работу декрементируя один и тот же счётчик - то да, LOCK DEC [COUNTER] справится с задачей.
    Более сложные сценарии когда потоки могут пытаться у друг друга выхватывать кусок памяти с которым надо поработать множественными инструкциями, то нужно реализовать спинлок, для этого уже придётся прибегать к CMPXCHG: https://ru.wikipedia.org/wiki/Сравнение_с_обменом
    И её тоже надо подпирать LOCK-ом.
    Однако если ресурс может быть занят огромное по меркам процессора время, то лучше потеребить спинлок несколько тысяч раз, и если не получилось захватить - уснуть в ядро.
    Так делает Critical Section из WinAPI и конечно проще пользоваться готовым примитивом нежели городить свои велосипеды.

    Есть еще такая дисциплина как "lock free" - типа как некоторые задачи типовые решать без простоев в виде вот этих вот "тысяч теребений" или "ухода в ядро". Но это уже курс со звёздочкой.
     
    Mikl___ нравится это.
  3. Ahimov

    Ahimov Active Member

    Публикаций:
    0
    Регистрация:
    14 окт 2024
    Сообщения:
    603
    Entropy,

    IA sdm: Locked Atomic Operations
    Для произвольной области памяти SRWL.
     
    Mikl___ нравится это.
  4. aa_dav

    aa_dav Active Member

    Публикаций:
    0
    Регистрация:
    24 дек 2008
    Сообщения:
    590
    А, и есть еще такая неприятная штука как memory barriers - современный процессор может переставлять инструкции в процессе разгребания конвеера во внеочередном исполнении и поэтому надо очень очень аккуратно относится к тому нет ли опасности что умный конвеер всё так переставит, что произойдёт обсёр на ровном месте при межпоточной синхронизации. Архитектура интелей самая дружелюбная в этом плане к программисту - у неё memory ordering самый строгий среди современных актуальных архитектур, но тоже не абсолютно строгий - надо иметь ввиду и нет нет да вставлять инструкцию заставляющую конвеер типа сперва завершить все чтения прежде чем начинать следующие за ними записи.
     
    Mikl___ нравится это.
  5. Marylin

    Marylin Active Member

    Публикаций:
    0
    Регистрация:
    17 фев 2023
    Сообщения:
    308
    Это исключено, т.к. примитивы подобного рода давно отшлифованы инженерами до блеска.
    И вообще планировщик старается исполнять программный тред на одном ядре SetThreadAffinityMask(), пуская поток его инструкций в конвейер именно этого ядра. Когда квант треда истекает, шедулер даёт доп.время на исполнение всех мопсов текущей инструкции в конвейере, чтобы не разорвать её пополам. Инструкция не уйдёт в отставку, пока все её мопсы не отработают.

    Перед тем-как отдать ядро вместе с его конвейером другому прог.треду, контекст предыдущего EIP/RIP сохраняется, конвейер полностью сбрасывается в нуль Flush, а грязные Dirty линейки кэша уходят в общий L3. Теперь новый тред заполняет чистый конвейер и кэш ядра уже своими данными, и так по кругу.

    Пинг-понг одного треда по разным ядрам стоит дорого - предыдущее ядро по внутренней шине IPI должно сообщить соседу, чтобы он забрал содержимое его кэшей и буферов TLB, поэтому ОС старается привязывать потоки Thread именно к одному ядру, а смена происходит только в исключительных случаях при большой нагрузке. "Обсер" случился-бы, если конвейер не очищался, а так можно об этом забыть. Межпоточная синхра организована на программном уровне намного выше, и аппаратный конвейер вообще не подозревает о ней.
     
    Mikl___ нравится это.
  6. aa_dav

    aa_dav Active Member

    Публикаций:
    0
    Регистрация:
    24 дек 2008
    Сообщения:
    590
    Так вопрос был про велосипеды руками в том числе. Если пользоваться готовыми примитивами, то да, там учтены и барьеры и локи.
    Вообще на x86 LOCK уже даёт барьер, поэтому еще проще нежели на ARM-ах например.
     
  7. Ahimov

    Ahimov Active Member

    Публикаций:
    0
    Регистрация:
    14 окт 2024
    Сообщения:
    603
    > умный конвеер всё так переставит,

    Практический вопрос: какой алгоритм для обнаружения подобных аномалий, кроме этого ?

    имхо академический матан, сводящийся к кэшам(задержкам), а не реальным данным. Последовательность инструкций не может быть нарушена, только если где errata.
     
  8. Marylin

    Marylin Active Member

    Публикаций:
    0
    Регистрация:
    17 фев 2023
    Сообщения:
    308
    Синхронизация потоков обеспечивается не одной кнопкой в конвейере, а сложной комбинацией спец.инструкций и протоколов когерентности кэшей. Когда инструкция сопровождается префиксом lock, блок управления памятью и кэш-контроллер блокируют доступ к соответствующей линии кэша для других ядер - это обеспечивает "неделимость" операции чтения/записи.

    У каждого ядра свои кэши L1/L2. Чтобы разные ядра видели одни и те же данные, используется протокол когерентности кэша MESI. Он следит за состоянием CacheLine, которая может иметь следующие флаги в своих тегах (название mesi взято по первым буквам):
    Код (Text):
    1. Modified  (M): Данные изменены только в этом ядре.
    2. Exclusive (E): Данные только в этом кэше, и не изменены.
    3. Shared    (S): Данные есть в кэшах нескольких ядер.
    4. Invalid   (I): Данные устарели.
    5.  
    Когда ядро(А) хочет изменить переменную по адресу(х), MESI аннулирует (Invalidates) эту же линию кэша во всех других ядрах. Теперь когда другое ядро(В) попытается прочитать эту переменную, оно увидит, что её кэш-копия невалидна, и будет вынуждено обратиться к памяти ОЗУ или к кэшу ядра-владельца, чтобы получить свежее значение. Это и есть базовая аппаратная синхра на уровне кэша. То-есть синхронизация потоков реализуется через когерентность кэша + атомарные инструкции.
     
    Mikl___ нравится это.
  9. aa_dav

    aa_dav Active Member

    Публикаций:
    0
    Регистрация:
    24 дек 2008
    Сообщения:
    590
    И забавно, что на ARM-ах подход диаметрально противоположный.
    Чтобы сделать атомарное чтение/запись сперва значение считывается особой инструкцией LDREX в регистр и эта инструкция метит кеш-линейку памяти как особенную для текущего ядра и начинает за ней следить. Далее серия инструкций поработав со значением должна его сохранить обратно инструкцией STREX и вот тут произойдёт следующее: если эта зачеканная кеш-линейка в этот момент никем больше не задета, то запись произойдёт и в качестве результата для проверки вернётся 0, а если же кто-то в эту кеш-линейку успел за это время насрать, то запись отменится и вернётся 1.
    Т.е. модель полностью противоположная - мы не настаиваем, а надеемся что получится и если не получилось, то надо пытаться и пытаться еще раз. :)
     
  10. Marylin

    Marylin Active Member

    Публикаций:
    0
    Регистрация:
    17 фев 2023
    Сообщения:
    308
    Копаясь в структуре KTHREAD, вчера наткнулся на сл.поля в ней, и вспомнил про эту тему.

    Код (Text):
    1. 0: kd> dt _KTHREAD fffffa8010812b50
    2. nt!_KTHREAD
    3. .......
    4.    +0x07c NextProcessor      : 3
    5.    +0x080 DeferredProcessor  : 2
    6.    +0x200 UserAffinity       : _GROUP_AFFINITY
    7.    +0x228 IdealProcessor     : 3      <--- ядро(4) отсчёт с нуля
    8.    +0x22c UserIdealProcessor : 3
    9.  
    10. 0: kd> dt _KTHREAD fffffa8010812b50 UserAffinity.
    11. nt!_KTHREAD
    12.    +0x200 UserAffinity  :
    13.       +0x000 Mask            : 0xf    <--- у меня 4 ядра = 1111b = 0xf
    14.       +0x008 Group           : 0
    15. 0: kd>
    А это те-же данные для второго треда, где рекомендуемым "Ideal" является уже ядро(3):

    Код (Text):
    1. 0: kd> dt _KTHREAD fffffa8010f1bb50
    2. nt!_KTHREAD
    3. ........
    4.    +0x07c NextProcessor      : 2
    5.    +0x080 DeferredProcessor  : 1
    6.    +0x228 IdealProcessor     : 2      <--- ядро(3)
    7.    +0x22c UserIdealProcessor : 2
    8. 0: kd>
    Дадим определение этим полям:

    1. NextProcessor (cледующее)
    Ядро, куда отправится поток при сл.переключении контекста.
    Устанавливается при пробуждении потока, и может отличаться от "Ideal" для баланса нагрузки.
    Шедулер может решить запустить поток на другом ядре, если идеальное занято.

    2. DeferredProcessor (отложенное)
    Временное сохранение номера ядра для отложенного переключения.
    Например поток(A) будит поток(B) на другом ядре. Но текущее не может сразу переключить контекст (допустим находится на высоком IRQL), тогда целевое(В) сохраняется в "DeferredProcessor". Вероятно в момент снятия дампа тулзой LiveKd, поток(А) находился в процессе миграции.

    3. IdealProcessor (предпочтительное)
    Планировщик пытается запустить поток именно на этом ядре,
    т.к. данные потока скорее всего лежат ещё в кэше L1/L2 этого ядра.

    4. UserIdealProcessor (рекомендуемое на уровне юзера)
    "Ideal" может меняться планировщиком системы динамически,
    а "UserIdeal" хранит последнее значение, которое мы сами задали через юзер SetThreadIdealProcessor().​

    Вот практический пример:

    Код (C++):
    1. // Устанавливаем идеальное ядро(2)
    2. SetThreadIdealProcessor(GetCurrentThread(), 2);
    3.  
    4. // Через некоторое время...
    5. // IdealProcessor     = 2  (что мы просили у системы)
    6. // UserIdealProcessor = 2  (запомнили наше требование)
    7.  
    8. // Но планировщик может решить...
    9. // NextProcessor      = 3  (это ядро сейчас менее загружено)
    10. // Тогда при пробуждении потока...
    11. // DeferredProcessor  = 1  (IPI уже послан на ядро(1) для переключения)
    Здесь "Next=2" и "Deferred=1" это несогласованное состояние.
    То-есть поток обычно работает на Ideal=2, теперь кто-то решил переместить его на Deferred=1, но ещё не завершил операцию Next=2. Это нормально для многоядрерных систем в динамике.