С ++11 атомика. visibility и thread.join() / правильный способ остановить поток
За что (если есть?) STORE_ORDER
& LOAD_ORDER
C++11 гарантирует, что этот код выполняется за конечное время?
std::atomic<bool> a{false};
std::thread t{[&]{
while(!a.load(LOAD_ORDER));
}};
a.store(true, STORE_ORDER);
t.join();
Я вижу две проблемы с этим:
Порядок памяти
Мне кажется, что с release
& aquire
, компилятор и процессор могут изменить порядок join
(при условии, что он ведет себя как нагрузка) до store
что, конечно, сломало бы это.
Даже с memory_order_seq_cst
Я не уверен, что такое изменение порядка запрещено, потому что я не знаю, join()
на самом деле делает какие-либо грузы или магазины.
видимость
Если бы я понял этот вопрос оmemory_order_relaxed
правильно, не гарантируется, что магазин с memory_order_relaxed
становится видимым для других потоков за конечное время. Есть ли такая гарантия для других заказов?
Я это понимаю std::atomic
о атомарности и упорядочении памяти, а не о видимости. Но я не знаю никаких других инструментов в C++11, которые могли бы мне здесь помочь. Нужно ли мне использовать инструмент для конкретной платформы, чтобы получить здесь гарантию правильности, и если да, то какую?
Чтобы сделать еще один шаг вперед - если бы у меня была конечность, было бы неплохо также пообещать скорость. Я не думаю, что стандарт C++ дает такие обещания. Но есть ли какой-то компилятор или специфичный для x86 способ получить обещание, что хранилище станет быстро видимым для другого потока?
В итоге: я ищу способ быстро остановить рабочий поток, который действительно имеет это свойство. В идеале это не зависит от платформы. Но если у нас не может быть этого, существует ли это по крайней мере для x86?
1 ответ
После еще нескольких поисков я нашел вопрос, идентичный моей части видимости, который получил четкий ответ: такой гарантии действительно нет - есть только запрос, что "реализации должны сделать атомарные хранилища видимыми для атомарных нагрузок внутри разумное количество времени ". Стандарт не определяет, что означает "должен", но я приму нормальное значение, так что это не будет обязательным. Также не совсем понятно, что означает "разумный", но я бы предположил, что он явно исключает "бесконечный".
Это не совсем отвечает на вопрос об упорядочении памяти. Но если магазин заказан после join()
, который может навсегда заблокировать, хранилище никогда не станет видимым для других потоков - что не будет "разумным количеством времени".
Таким образом, хотя стандарт не требует, чтобы код в вопросе был действительным, он по крайней мере предполагает, что он должен быть действительным. В качестве бонуса, это на самом деле говорит о том, что это должно быть не только конечное время, но и несколько быстрое (или, конечно, разумное).
Это оставляет часть моего вопроса о платформо-зависимом решении: существует ли специфичный для x86 способ написания запрошенного алгоритма, чтобы он гарантированно был правильным?
Есть ли специфичный для x86 способ написать запрошенный алгоритм, чтобы гарантировать его правильность?
Используйте исправный компилятор, чтобы убедиться, что thread.join()
правильно обрабатывается как функция "черного ящика", которая может читать или записывать любую память.
Таким образом, компилятор должен будет убедиться, что память "синхронизирована с абстрактной машиной C++, прежде чем блокировать завершение потока. То есть переупорядочение хранилища во время компиляции может нарушить правило" как если бы ", поэтому компиляторы не должны этого делать, если они не могут доказать, что он не будет (например, в локальную переменную, адрес которой не экранирован функцией). В данном случае это не так, поэтому передcall join
даже для mo_relaxed
.
(Или в гипотетической реализации join
который мог бы быть полностью встроенным, по крайней мере, имел бы барьер памяти во время компиляции, такой как GNU C asm("":::"memory")
или, может быть atomic_thread_fence()
некоторой силы. Все, что до acq_rel, не требует asm-инструкций на x86, просто блокирует переупорядочение во время компиляции.)
Ядра ЦП x86 совместно используют согласованное представление памяти через согласованный кеш.(Протокол MESI или эквивалент). Как только хранилище фиксируется из буфера хранилища в кэш L1d, никакое другое ядро становится невозможным для чтения "устаревшего" значения. Межъядерная задержка обычно составляет от 40 до 100 наносекунд на современных x86, IIRC (когда оба потока уже работают на разных физических ядрах).
См. Безопасно ли mov + mfence на NUMA? и когда использовать volatile с многопоточностью? объясните больше о том, как asm не может бесконечно видеть устаревшие значения на реальных процессорах (включая не x86). Так что здесь достаточно порядка программ во время компиляции.
То же самое относится и к любому другому реальному процессору, с которым может работать нормальная реализация C++, используя std::thread
. (Там являются некоторые гетерогенные системы на кристалле процессоры с отдельными доменами когерентности между микроконтроллером и DSP, но качество C++ реализация - хstd::thread
не запускал потоки через некогерентные ядра.
Или, если реализация работала на ядрах без связного кеша, ее std::atomic
пришлось бы очистить строку кеша после расслабленного атомарного хранения. А может до расслабленно-атомной нагрузки. Или синхронизировать / записать любые грязные данные во всем кэше перед выпуском-хранением. Так что было бы до смешного неэффективно реализовать модель памяти C++ поверх системы некогерентной / явной когерентности. Он должен быть достаточно согласованным, чтобы атомарный RMW работал, как описано (последовательности выпуска и так далее, и в любой момент времени существует только одна "живая" копия атомарного счетчика).
Вот почему вы бы не стали создавать такую реализацию на C++. Вы можете разрешить запускать вещи на других ядрах, которые являются частью другого домена когерентности (ARM не "разделяемый внутри"), но не через std::thread, потому что вам нужно, чтобы пользователи знали последствия, если вы не хотите общие переменные разумно работают между ними.