Атомарные права доступа что это

Обновлено: 21.05.2024

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

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

Итак, есть у нас такой код (не ищите в нем практического смысла, я его просто как пример работы с разными операндами написал):

volatile char flag_byte; /*Просто флаговая переменная, на разные случаи жизни. Разные события там выставляют флажки, опираясь на которые потом работает логика программы. Один из способов организации псевдомногозадачности. Когда у нас главный цикл анализирует флажки и делает переходы на подпрограммы, а вызов подпрограмм осуществляется не напрямую, а установкой соответствющих флажков. Своего рода диспетчер переходов. О такой архитектуре я скоро расскажу)*/ ISR (USART_RXC_vect) // Обработчик прерывания, самый обычный. < flag_byte|=1
Человек ни разу не писавший под МК на ассемблере, начавший изучать МК сразу с Си, схавает такую конструкцию и даже не поморщится. Прожженый ассемблерщик же нецензурно матюгнется, обзовет первого быдлокодером и исправит исходник следующим образом:

volatile char flag_byte; // Просто флаговая переменная под флаги на разные случаи жизни ISR (USART_RXC_vect) // Обработчик прерывания < flag_byte|=1 одной замечательной статье , она хоть и про PIC24, но полезна будет всем.

Так что мало знать что есть вилы, надо уметь их обходить с минимальными потерями. Вот для этого то и надо хорошо знать ассемблер того контроллера на котором пишешь, на уровне хотя бы десятка — двух программ отличных от тупой мигалки светодиодом. Чтобы сразу за сишным кодом видеть возможные ассемблерные инструкции, знать что будет делать компилятор в том или ином случае. Где можно ожидать вилы в стоге сена.

Отлично, враг известен, чем его мочить тоже выяснили. Но любое оружие может замочить и хозяина. Если им неумело пользоваться. Поясню на примере:

Вот сделали мы какую нибудь функцию, а чтобы прерывания нам не нагадили мы добавили в них конструкцию cli/sei на критичные места. Все довольны, все счастливы. Ага, до тех пор пока в целях оптимизации и универсальности кода мы не вкатим эту функцию в обработчик прерывания. В принципе, ничего страшного нет, пользоваться в прерыванях функциями можно. Вот только надо учитывать что в прерываниях прерывания же аппаратно запрещены, а наша функция по выходу из критических мест их разрешает. И тут у нас вылезают другие вилы — вложенные прерывания. Это бага не столь законспирированная как неатомарный доступ, но вылезти тоже может ой как не сразу.

Что делать? А тут все просто. Если у нас есть функция с атомарными операциями и юзается она и в прерываниях и в основном теле программы, то надо бы это учитывать и не менять флаг прерывания зря. Т.е. если он был сброшен (в прерывании) то мы его и не возвращаем обратно — незачем.

На ассемблере это может выглядеть, например, так:

; Begin Atomic Block IN R17,SREG ; Сохранили регистр SREG, а в нем и флаг I PUSH R17 ; можно в стеке, можно еще где. Не принципиально CLI ; Запретили прерывания . ; Тут у нас код который должен быть атомарным . ; . ; POP R17 ; Достали сохраненное в стеке OUT SREG,R17 ; Вернули SREG на место ; End Atomic Block

Разрешать прерывания тут не надо. Т.к. мы сохранили сразу весь SREG. Если прерывания были разрешены, то по доставании его из стека они достанутся разрешенными. Ну а если не были разрешены то ничего и не изменится.

На Си же можно тупо в лоб проверять условием наличие флага I регистра SREG и в зависимости от этого делать потом разрешение прерывания или нет. Запрещать их в любом случае. Либо применить инлайновый ассемблер своего Си компилятора.

А еще можно поискать уже готовые макросы в составе компилятора.

В WinAVR GCC например есть такой хидер как util/atomic.h

где есть макросы:

Где в качестве type возможны два варианта: ATOMIC_FORCEON — прерывания по выходу из атомарного блока будут включены и ATOMIC_RESTORESTATE в котором состояние флага I будет таким же какое и на входе в блок. Запрещены — так запрещены, разрешены так разрешены. Но работает чуть медленней и памяти требует больше.

Также там есть макрос разатомаривания.

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

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

Так что можно вместо:

cli(); flag_byte |=1 отличная статья Виктора Тимофеева про то как надо правильно писать код. Настоятельно рекомендую к прочтению.

Спасибо. Вы потрясающие! Всего за месяц мы собрали нужную сумму в 500000 на хоккейную коробку для детского дома Аистенок. Из которых 125000+ было от вас, читателей EasyElectronics. Были даже переводы на 25000+ и просто поток платежей на 251 рубль. Это невероятно круто. Сейчас идет заключение договора и подготовка к строительству!

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

Атомарная (атом от греч. atomos — неделимое) операция — операция, которая либо выполняется целиком, либо не выполняется вовсе; операция, которая не может быть частично выполнена и частично не выполнена.

В данной статье описываются простейшие атомарные операции (чтение, запись и т. п.), хотя термин может относиться к более высокоуровневым операциям, таким как, например, серия запросов к СУБД в рамках одной транзакции.

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

Классификация

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

Ассемблерные инструкции и атомарность

Машинные инструкции, выполнение которых всегда можно считать атомарным:

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

Машинные инструкции, которые не являются атомарными:

Атомарные инструкции процессоров x86

Атомарные инструкции процессоров архитектуры x86:

  • CMPXCHG, CMPXCHG8B, CMPXCHG16B — основная атомарная инструкция процессоров x86, выполняющая сравнение и обмен. При использовании с префиксом LOCK атомарно сравнивает значение переменной с указанным значением и, в зависимости от результата сравнения, записывает в переменную указанное значение или ничего не делает. Является основой реализации всех безблокировочных алгоритмов, часто используется в реализации спинлоков, RWLock’ов и практически всех высокоуровневых синхронизирующих элементов, таких как семафоры, мьютексы, события и пр.;
  • XCHG — операция для обмена данными между регистром и ячейкой памяти или между двумя регистрами. Атомарность этой операции имеет значение в случае, когда операндом команды выступает ячейка памяти. На процессорах x86 выполняется атомарно даже без использования префикса LOCK (по этой причине следует избегать применения этой команды просто для обмена значений регистра и ячейки памяти, это вызовет ненужные и весьма существенные задержки выполнения кода). Часто используется в реализации спинлоков.
  • команды сложения и вычитания ADD, ADC, SUB и SBB в случае, если операнд-приёмник — адрес ячейки памяти;
  • команды инкремента и декремента INC и DEC;
  • логические команды AND, OR и XOR;
  • однооперандные команды NEG и NOT;
  • битовые операции BTS, BTR и BTC;
  • операция сложения и обмена XADD.

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

Атомарные инструкции процессоров RISC

  • загрузка с пометкой (LL — load linked);
  • изменение данных;
  • попытка записи (SC — store conditional).

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

Атомарные инструкции и компиляторы

Компиляторы языков высокого уровня, как правило, не используют при генерации кода атомарные инструкции, поскольку, во-первых, атомарные операции во много раз более ресурсоёмкие, чем обычные, а во-вторых, у компилятора нет информации о том, когда доступ к данным должен осуществляться атомарно (так как даже модификатор volatile для переменной в языках C/C++ не означает реальной необходимости применения атомарных операций). В случае необходимости программист может использовать атомарные инструкции одним из следующих способов:

Узнайте, как использовать атомарные переменные для решения проблем параллелизма.

1. введение

Проще говоря, общее изменяемое состояние очень легко приводит к проблемам, когда задействован параллелизм. Если доступ к общим изменяемым объектам не управляется должным образом, приложения могут быстро стать подверженными некоторым труднодоступным ошибкам параллелизма.

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

2. Замки

Давайте взглянем на класс:

В случае однопоточной среды это работает идеально; однако, как только мы разрешаем писать более одного потока, мы начинаем получать противоречивые результаты.

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

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

Одним из способов управления доступом к объекту является использование блокировок. Это может быть достигнуто с помощью ключевого слова synchronized в сигнатуре метода increment . Ключевое слово synchronized гарантирует, что только один поток может одновременно ввести метод (подробнее о блокировке и синхронизации см. – Руководство по ключевому слову Synchronized в Java ):

Кроме того, нам нужно добавить ключевое слово volatile , чтобы обеспечить надлежащую видимость ссылок между потоками.

Использование блокировок решает проблему. Тем не менее, производительность принимает удар.

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

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

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

3. Атомарные операции

Существует раздел исследований, посвященный созданию неблокирующих алгоритмов для параллельных сред. Эти алгоритмы используют низкоуровневые атомарные машинные инструкции, такие как compare-and-swap (CAS), для обеспечения целостности данных.

Типичная операция CAS работает с тремя операндами:

  1. Ячейка памяти, в которой выполняется операция (M)
  2. Существующее ожидаемое значение (А) переменной
  3. Новое значение (B), которое необходимо установить

Операция CAS атомарно обновляет значение в M до B, но только в том случае, если существующее значение в M совпадает с A, в противном случае никаких действий не предпринимается.

В обоих случаях возвращается существующее значение в M. Это объединяет три этапа – получение значения, сравнение значения и обновление значения – в одну операцию на уровне машины.

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

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

4. Атомарные переменные в Java

Наиболее часто используемыми классами атомарных переменных в Java являются AtomicInteger , AtomicLong , AtomicBoolean и AtomicReference . Эти классы представляют собой int , long , boolean, и ссылку на объект соответственно, которые могут быть атомарно обновлены. Основными методами, предоставляемыми этими классами, являются:

  • get() – получает значение из памяти, чтобы изменения, внесенные другими потоками, были видны; эквивалентно чтению переменной volatile
  • set() – записывает значение в память, чтобы изменение было видно другим потокам; эквивалентно записи переменной volatile
  • lazySet() – в конечном итоге записывает значение в память, возможно, переупорядоченное с последующими соответствующими операциями с памятью. Один из вариантов использования-аннулирование ссылок ради сбора мусора, к которому больше никогда не будет доступа. В этом случае лучшая производительность достигается за счет задержки записи null volatile
  • compareAndSet() – то же, что описано в разделе 3, возвращает true при успешном выполнении, иначе false
  • weakCompareAndSet() – то же самое, что описано в разделе 3, но слабее в том смысле, что он не создает заказы перед заказами. Это означает, что он может не обязательно видеть обновления, внесенные в другие переменные. Начиная с Java 9 этот метод устарел во всех атомарных реализациях в пользу weakCompareAndSet Plain() . Эффекты памяти weakCompareAndSet() были простыми, но их названия подразумевали эффекты изменчивой памяти. Чтобы избежать этой путаницы, они устарели этот метод и добавили четыре метода с различными эффектами памяти, такими как weakCompareAndSetPlain() или weakCompareAndSetVolatile()

Потокобезопасный счетчик, реализованный с помощью AtomicInteger , показан в примере ниже:

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

5. Заключение

В этом кратком руководстве мы описали альтернативный способ обработки параллелизма, при котором можно избежать недостатков, связанных с блокировкой. Мы также рассмотрели основные методы, предоставляемые классами атомарных переменных в Java.

Как всегда, все примеры доступны на GitHub .

Чтобы изучить больше классов, которые внутренне используют неблокирующие алгоритмы, обратитесь к руководству по ConcurrentMap .

Что atomic и nonatomic означает в декларациях собственности?

В чем заключается оперативная разница между этими тремя?

Предполагая, что вы @synthesizing реализации метода, атомарный против неатомарного изменения сгенерированного кода. Если вы пишете свой собственный установщик / получатель, атомарный / неатомный / сохранить / назначить / копировать просто рекомендации. (Примечание: @synthesize теперь является поведением по умолчанию в последних версиях LLVM. Также нет необходимости объявлять переменные экземпляра; они также будут автоматически синтезироваться и иметь _ префикс перед именем для предотвращения случайного прямого доступа).

Обеспечение целостности данных - одна из основных задач многопоточного программирования - достигается другими средствами.

Добавление к этому:

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

В этом случае поток A может переименовывать объект, вызывая setFirstName: и затем вызывая setLastName: . Между тем, поток B может вызывать fullName между двумя вызовами потока A и получит новое имя в сочетании со старой фамилией.

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

Учитывая, что любой потокобезопасный код будет выполнять свою собственную блокировку и т. Д., Когда вы захотите использовать методы доступа к атомарным свойствам? У меня проблемы с придумыванием хорошего примера.

Вот пример, аналогичный @StevenKramer's: у меня есть @property NSArray* astronomicalEvents; список данных, которые я хочу отобразить в пользовательском интерфейсе. Когда приложение запускает указатель на пустой массив, приложение извлекает данные из Интернета. Когда веб-запрос завершается (в другом потоке), приложение создает новый массив, а затем атомарно присваивает свойству новое значение указателя. Это потокобезопасно, и мне не нужно было писать код блокировки, если я что-то упустил. Кажется довольно полезным для меня.

@HotLicks Еще один забавный; на некоторых архитектурах (не помню, какая именно) 64-битные значения, передаваемые в качестве аргумента, могут передаваться наполовину в регистре и наполовину в стеке. atomic предотвращает перекрестное чтение половинных значений. (Это была забавная ошибка, чтобы выследить.)

@congliu Тема A возвращает объект без retain/autorelease танца. Поток B освобождает объект. Нить А идет бум . atomic гарантирует, что поток A имеет сильную ссылку (число сохранений +1) для возвращаемого значения.

Это объясняется в документации Apple , но ниже приведены некоторые примеры того, что на самом деле происходит.

Теперь атомный вариант немного сложнее:

По сути, атомарная версия должна блокироваться, чтобы гарантировать безопасность потока, а также увеличивает счетчик ссылок на объект (и счет автоматического выпуска, чтобы сбалансировать его), так что объект гарантированно существует для вызывающей стороны, в противном случае является потенциальным условием гонки, если другой поток устанавливает значение, в результате чего счетчик ссылок падает до 0.

@Louis Gerbarg: Я полагаю, что ваша версия (неатомного, сохраняющего) сеттера не будет работать должным образом, если вы попытаетесь назначить один и тот же объект (то есть: userName == userName_)

@ fyolnish Я не уверен, что _val / val есть, но нет, не совсем. Метод получения атомарного свойства copy / retain свойства должен гарантировать, что он не возвращает объект, чей refcount становится равным нулю из-за того, что метод вызова вызывается в другом потоке, что по сути означает, что он должен прочитать ivar, сохранить его, при этом убедившись, что метод установки не имеет перезаписать и отпустить его, а затем автоматически выпустить, чтобы сбалансировать сохранение. По сути, это означает, что и получатель, и установщик должны использовать блокировку (если структура памяти была исправлена, это должно быть осуществимо с помощью инструкций CAS2; увы -retain , это вызов метода).

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

атомное

  • поведение по умолчанию
  • обеспечит завершение текущего процесса ЦП, прежде чем другой процесс получит доступ к переменной
  • не быстрый, так как он гарантирует, что процесс завершен полностью

Неатомарный

  • НЕ по умолчанию
  • быстрее (для синтезированного кода, то есть для переменных, созданных с использованием @property и @synthesize)
  • не потокобезопасный
  • может привести к непредвиденному поведению, когда два разных процесса одновременно обращаются к одной и той же переменной

Лучший способ понять разницу - использовать следующий пример.

Если свойство "name" было неатомичным, то все потоки в приведенном выше примере - A, B, C и D будут выполняться одновременно, что приведет к непредсказуемому результату. В случае атомарного, один из A, B или C будет выполняться первым, но D все еще может выполняться параллельно.

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

В чем функциональная разница между этими 3?

Я всегда считал атомную по умолчанию довольно любопытной. На уровне абстракции, над которым мы работаем, использование атомарных свойств для класса в качестве транспортного средства для достижения 100% -ной безопасности потока является ключевым случаем. Для действительно правильных многопоточных программ вмешательство программиста почти наверняка является требованием. Между тем, характеристики производительности и исполнения еще не были подробно описаны. Написав несколько многопоточных программ за эти годы, я все время заявлял о своих свойствах, nonatomic потому что atomic не был пригоден для каких-либо целей. Во время обсуждения деталей атомных и неатомных свойств этого вопроса я при профилировании столкнулся с некоторыми любопытными результатами.

выполнение

Хорошо. Первое, что я хотел бы прояснить, это то, что реализация блокировки определяется реализацией и абстрагируется. Луи использует @synchronized(self) в своем примере - я видел это как общий источник путаницы. Реализация на самом деле не использует @synchronized(self) ; он использует спин-блокировки на уровне объекта . Иллюстрация Луи хороша для иллюстрации высокого уровня с использованием конструкций, с которыми мы все знакомы, но важно знать, что она не используется @synchronized(self) .

Другое отличие состоит в том, что атомарные свойства будут сохранять / освобождать цикл ваших объектов в геттере.

Представление

Вот интересная часть: производительность, использующая доступ к элементарным свойствам в неоспоримых (например, однопоточных) случаях, в некоторых случаях может быть действительно очень высокой. В далеко не идеальных случаях использование атомарного доступа может стоить более чем в 20 раз дороже nonatomic . В случае оспариваемого случая, использующего 7 потоков, для трехбайтовой структуры было в 44 раза медленнее (2,2 ГГц Core i7 Quad Core, x86_64). Трехбайтовая структура является примером очень медленного свойства.

Интересное примечание: определяемые пользователем средства доступа трехбайтовой структуры были в 52 раза быстрее, чем синтезированные атомарные средства доступа; или 84% скорости синтезированных неатомных акцессоров.

Объекты в оспариваемых случаях также могут превышать 50 раз.

Итак, давайте сделаем шаг назад, не сосредотачиваясь на реализации доступа к свойствам, мы включим обычные подозреваемые, такие как objc_msgSend , и исследуем некоторые реальные результаты высокого уровня для многих вызовов NSString геттера в неоспоримом случаях (значения в секундах):

  • MRC | неатомный | Внедренные вручную геттеры: 2
  • MRC | неатомный | синтезированный геттер: 7
  • MRC | атомный | синтезированный геттер: 47
  • ARC | неатомный | синтезированный геттер: 38 (примечание: ARC добавляет количество циклов отсчета здесь)
  • ARC | атомный | синтезированный геттер: 47

Как вы, наверное, догадались, активность / циклический подсчет ссылок вносит существенный вклад в атомарность и в ARC. Вы также увидите большие различия в спорных случаях.

Хотя я уделяю пристальное внимание производительности, я по-прежнему говорю Semantics First! , Между тем, производительность является низким приоритетом для многих проектов. Однако знание деталей исполнения и стоимости используемых вами технологий, безусловно, не повредит. Вы должны использовать правильную технологию для ваших потребностей, целей и способностей. Надеемся, что это сэкономит вам несколько часов сравнений и поможет вам принять более обоснованное решение при разработке ваших программ.

Читайте также: