2016-11-26 5 views
1

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

x.load(std::memory_order_relaxed)

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

// Thread 1: 
x.store(33, std::memory_order_release); 

// Thread 2: 
x.load(std::memory_order_acquire) 

Будет ли резьба 2 всегда видеть свежую ценность в этом случае?

Теперь, если мы добавим третий поток, который выполняет расслабленное хранилище, в предыдущий пример, поток 2 может не видеть это обновление, поскольку синхронизация устанавливается только между потоками 1 и 2. Я прав?

И, наконец, операции чтения-изменения-записи всегда работают с новыми значениями. Означает ли это, что они заставляют поток «видеть» обновления, сделанные другими потоками, поэтому, если мы загрузим после операции чтения-изменения-записи, мы увидим, что значение не менее свежо, чем эта операция?

// Thread 1: 
x.fetch_add(1, std::memory_order_relaxed); // get the fresh value and add 1 
if (x.load(std::memory_order_relaxed) == 3) // not older than fetch_add 
    fire(); 

// Thread 2: 
x.fetch_add(2, std::memory_order_relaxed); // will see thread 1 modification 
if (x.load(std::memory_order_relaxed) == 3) // will see what fetch_add has put in x or newer 
    fire(); 

Для x изначально нулевой я могу быть уверен, что fire будет называться по крайней мере один раз в этом случае? Все мои тесты показали, что это работает, но, возможно, это вопрос моего компилятора или аппаратного обеспечения.

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

+0

Вы не знаете сроки разных потоков. Если поток 1 записывается в переменную, а поток 2 читает эту переменную, вы не знаете, читает ли поток 2 _before_ или _after_ записи. Atomic может помочь убедиться, что поток 2 не читает _during_ считывания и получает смесь старого и нового значения; это все. – gnasher729

+0

Какую часть вопроса вы имеете в виду? – mentalmushroom

+0

re: последний абзац: переупорядочение памяти во время выполнения и времени компиляции и исполнение вне порядка, все сохраняют поведение одного потока. Золотое правило всего этого материала (в том числе правило «как есть» компилятора) - «не разбивать однопоточный код». Однако хранимая часть 'x.fetch_add' может стать глобально видимой для * других * потоков после' x.load'. Он не будет на x86, поскольку x86 не переупорядочивает магазины с более поздними загрузками с одного и того же адреса (но переупорядочение StoreLoad разрешено для других адресов). –

ответ

2

Ключевым моментом является то, что memory_order_relaxed означает, что fetch_add не заказывает грузы/магазины любым другим местонахождение. Он делает все, что нужно, чтобы быть атомарным и не более. Тем не менее, порядок зависимостей в одном потоке по-прежнему применяется.


Для fetch_add атомарным, она должна предотвратить любой другой fetch_add пытается выполнить в то же время от работающих на том же входном значении это чтение. В значительной степени все использует производную от MESI cache-coherence protocol, поэтому на практике атомный fetch_add выполняется путем сохранения строки кэша «заблокирован» в состоянии M от чтения до записи. (Или с LL/SC, обнаружив, что этого не произошло и повторите попытку.) См. Также Can num++ be atomic for 'int num'? для более подробного описания для x86.

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


Для x изначально нулевой, я могу быть уверен, что fire будет называться по крайней мере один раз в этом случае?

Да, он будет работать хотя бы один раз. Он может работать два раза, так как для обеих нагрузок можно увидеть результат со второго fetch_add.

Нагрузки всегда видят значение для x, которое было обновлено, по крайней мере, fetch_add в своем потоке. memory_order_relaxed не позволяет нить наблюдать за собственными операциями, происходящими не по порядку, и fetch_adds происходят в определенном порядке. Итак, по крайней мере, поток, который «идет вторым», увидит x == 3.

Непонятно, какой заказ будет. Вы можете наблюдать за ним, глядя на возвращаемое значение fetch_add вместо использования отдельной нагрузки. (Этот код не соблюдает порядок, установленный в fetch_add с, потому что несколько потоков могут получить такое же значение. Для этого вам нужно захватить значение в рамках одной атомарной операции.)


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

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

Но порядок, в котором эти операции становятся глобально видимыми для других потоков, не гарантируется. Хранилище x.fetch_add может стать глобально видимым до других нитей после x.load. Он не будет на x86, поскольку x86 не переупорядочивает магазины с более поздними загрузками с одного и того же адреса (но переупорядочение StoreLoad разрешено для других адресов, even when a store and load partially overlap.)

Третья нить может видеть операции T1 и T2 как происходит в другом порядке, чем T1 видит операции T2. (Т. Е там не должно быть в общей сложности порядка, что все потоки согласования, если вы не используете последовательную согласованность)

Обратите внимание, что «наблюдения» нагрузка становится глобально видимым возможно только косвенно: глядя на другие магазины, сделанные потоком, чтобы выяснить, какое значение должно быть загружено. Но нагрузка - это настоящая и важная часть заказа.

Так что, если fire() пишет что-то в память, 3-я нить могла видеть, что происходит, пока она еще видит x==0. Это возможно только в том случае, если он выглядит (в потоке 3), как нагрузка T1, произошедшая до fetch_add в любом потоке. Даже если поток 3 использовал x.fetch_add(mo_relaxed) для наблюдения за значением в x, C++ допускает это. Но, как я уже сказал, вы не увидите его на x86.


Обратите внимание, что «порядок зависимостей» - это фраза с другим значением. Даже на слабоупорядоченных процессорах, загружая указатель, а затем разыменовывая его, он гарантирует, что переупорядочение LoadLoad не произойдет. Существует только одна архитектура, которая этого не делала: Alpha требует барьера для этого. Это одна из причин, по которой memory_order_consume и memory_order_relaxed являются отдельными.

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

Значение отношения зависимостей применяется между двумя разными ячейками памяти: указателем и буфером. Это совсем другое дело в том, что поток всегда видит свои собственные действия в порядке.


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

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

Есть некоторые ссылки в и теги wikis. Особенно рекомендую Jeff Preshing's blog. Он хорошо объясняет, и опубликовал много хороших статей.

+0

Спасибо за столь подробное объяснение! Хотя мне все еще трудно понять некоторые моменты (например, указатели загрузки и разыменования, поскольку, как я помню, мне говорят, что указанные данные могут быть не самыми современными, несмотря на то, что указатель есть - см. № 2 [здесь] (http://stackoverflow.com/questions/40127280/lock-free-programming-reordering-and-memory-order-semantics)), в общем, вы все поняли для меня. Я прочитаю больше из ваших ссылок. – mentalmushroom

+0

@mentalmushroom: для загрузки + deref для работы вам необходимо использовать магазин-релиз в продюсере. Я забыл упомянуть об этом! Но тогда вам нужно только «mo_consume» у потребителя, а не 'mo_acquire'. –