Хочу убедиться, что правильно понимаю порядки, поэтому спрашиваю ваше мнение о нескольких ситуациях ниже:
1 - Once. Предположим, что надо написать Once только с двумя значениями (WasNotCalled = 0, WasCalled = 1). Для метод call может использовать swap и вернуть предыдущее значение. Но какой порядок нужно использовать в этом swap?
2 - SmartMutex. Предположим, что у нас есть очередь ожидающих задач. Тогда мы можем записать длину этого списка в SmartMutex. Если она равна -1 мы можем захватить блокировку. Если она равна 0, мы можем подождать пару спинов и попробовать снова. Если она больше 0, мы можем только встать в очередь.
Соответственно, нужно написать 2 метода: первый пробует взять лок, но не встаёт в очередь. То есть он должен сделать CAS и посмотреть на вернувшееся значение (если > 0 вернуть None). Какие порядки нужны этому CAS?
Второй метод увеличивает счётчик на один и встаёт в очередь, если предыдущее значение не -1, иначе возвращает блокировку. Какой порядок нужно использовать тут?
Я ничего не знаю про rust, но это же вопрос про memory model, а эта тема уже обсосана C и С++. Например: eel.is/c++draft/atomics.order Самый строгий вариант ордеринга (sequential consistency) будет работать корректно всегда, и если вопрос не стоит как "насколько его можно ослабить", то разумно использовать его. Кроме того, для CAS это же единственная возможная опция, наксколько мне известно.
jcmvbkbc, использовать всегда самый строгий вариант является довольно спорным решением. Хотелось бы использовать настолько слабый вариант, насколько это возможно.
Я тоже читал, что Rust использует такую же модель, как в C, но в Rust можно использовать не только SeqCst в CAS операциях. По крайней мере в compare_exchange, который используется как альтернатива устаревшему compare_and_swap. Но он принимает 2 порядка, так что может иметь и другие различия. Например, для захвата блокировки обычно используют Acquire + Relaxed.
Eugene Usachev, говоря, что для CAS это единственная опция я имел в виду вот что. Acquire говорит о том, что операция чтения должна обозреть произошедшие записи в память, Release говорит о том, что операция записи должна опубликовать произведённые изменения. Поскольку CAS -- это обе эти операции, то для него имееют смысл только Relaxed и Acquire + Release, что эквивалентно SeqCst. Relaxed -- это по сути ничего, ни упорядочивания, ни публикации изменений. Acquire и Release имеет смысл применять поотдельности только тогда, когда между ними есть что-то ещё. Это не твой первый случай (где нужно просто атомарно установить значение 1 если было 0 и вернуть, что было). И это не твой второй случай (где нужно в одном месте атомарно увеличить значение и вернуть что было, а в другом -- атомарно уменьшить).
jcmvbkbc, мы довольно забавно обменялись очевидными фактами : ) Вопрос про порядки всё ещё остаётся открытым. Выглядит будто в первом случае нужно использовать AqrRel, Acquire + Acquire во втором и AcqRel в последнем. Второй случай я больше отдал для проверки, чем для советов, так как в нём я уверен. А вот в первом и последнем я боюсь, что взял слишком строгий порядок.
но в Rust можно использовать не только SeqCst в CAS операциях
В С/С++ в CAS то же можно использовать разные варианты, на свое усмотрение.
На сколько я понимаю схему работы memory ordering, то их нужно применять когда:
1. после записи в атомарную переменную вы должны опубликовать текущие изменения значений других переменных то же
2. после чтения атомарной переменно вы должны получить измененные значения других переменных.
Т.е. если вам нужно работать только со значением самой атомарной переменной и окружение не важно, то можно использовать relaxed - синхронизация атомарной переменной будет обеспечена и в этом случае.
Поэтому для вашего первого примера вполне достаточно relaxed.
На счет второго - для работы только со счетчиком можно использовать и relaxed, но т.к. мьютекс должен обеспечивать после захвата синхронизацию памяти для внешних переменных пользователя, то я бы делал SeqCst или Acq. Либо в самом CAS использовать Relaxed, но если мьютекс захватываем то дополнительно добавить синхронизацию atomic_thread_fence(SeqCst) (думаю в rust есть свой аналог).
Но вообще нужно смотреть на код, т.к. даже если вы не захватываете мьютекс, то вам нужно поместить поток в очередь, а сама очередь - это то же разделяемые данные, которые можно синхронизировать в CAS или отдельно. Либо очередь должна быть потокобезопасной сама по себе.
Вообще можно делать так - в первоначальном варианте задавать везде SeqCst. Когда алгоритм заработает, то можно поиграть на ослабление, по одной меняя упорядочивание и после каждой замены тестировать работу. В любом случае атомарные алгоритмы требуют тщательного тестирования.
Предположим, что надо написать Once только с двумя значениями (WasNotCalled = 0, WasCalled = 1). Для метод call может использовать swap и вернуть предыдущее значение. Но какой порядок нужно использовать в этом swap?
В такой постановке вопрос не имеет смысла, потому что для атомарного изменения одной переменной порядок не имеет значения. Порядок имеет значение, когда надо установить отношение между доступом к памяти и чем-то ещё и в зависимости от этого отношения он может быть разным.
Например, если этот once используется, чтобы гарантировать однократную запись в какой-нибудь регистр. Т.е. наблюдение once = 0 говорит о том, что никто не записал и пока не планирует записывать, а once = 1 говорит о том, что запись уже произошла или произойдёт в будущем. Это можно реализовать как CAS 0->1 для once + последующая запись в регистр, если CAS был успешен. Тогда нужно использовать как минимум release при записи в once и consume при чтении из него, чтобы эти две операции синхронизировались друг с другом при исполнении разными агентами. А так же гарантировать, что запись в регистр не будет переупорядочена с записью в once, например барьером между записью в once и записью в регистр.
Я уверен, что двумя значениями once (0 и 1) нельзя обеспечить одновременно единственность вызова и предоставить информацию о том, что вызов уже совершён. Для этого нужно как минимум три значения.