Назначает указатель в программе C, которая считается atomi c на x86-64 - PullRequest
7 голосов
/ 03 августа 2020

https://www.gnu.org/software/libc/manual/html_node/Atomic-Types.html#Atomic -Types говорит - На практике вы можете предположить, что int - это atomi c. Вы также можете предположить, что типы указателей являются атомарными; это очень удобно. Оба эти предположения верны на всех машинах, которые поддерживает библиотека GNU C, и на всех известных нам системах POSIX.

Мой вопрос в том, можно ли считать присвоение указателя atomi c на архитектуре x86_64 для программы C, скомпилированной с флагом g cc m64. ОС - 64-битная Linux, а ЦП - Intel (R) Xeon (R) CPU D-1548. Один поток будет устанавливать указатель, а другой поток будет обращаться к указателю. Есть только одна ветка писателя и одна ветка читателя. Читатель должен получать либо предыдущее значение указателя, либо последнее значение, а не значение мусора между ними.

Если это не считается atomi c, дайте мне знать, как я могу использовать g cc atomi c builtins или, возможно, барьер памяти, такой как __sync_synchronize, чтобы добиться того же без использования блокировок. Интересует только решение C а не C ++. Спасибо!

Ответы [ 4 ]

6 голосов
/ 03 августа 2020

Имейте в виду, что одной атомарности недостаточно для взаимодействия между потоками. Ничто не мешает компилятору и CPU переупорядочивать предыдущие / последующие инструкции загрузки и сохранения в этом хранилище «atomi c». Раньше люди использовали volatile, чтобы предотвратить это переупорядочение, но это никогда не предназначалось для использования с потоками и не предоставляет средств для указания менее или более ограничительного порядка памяти (см. «Связь с volatile» там).

Вы должны использовать атомарность C11, потому что они гарантируют как атомарность, так и порядок памяти.

3 голосов
/ 03 августа 2020

Почти для всех архитектур загрузка и сохранение указателя - это atomi c. Когда-то заметным исключением были 8086/80286, где указатели могли быть seg: offset; была инструкция l [des], которая могла выполнить загрузку atomi c; но нет соответствующего atomi c store.

Целостность указателя - лишь небольшая проблема; ваша большая проблема связана с синхронизацией: указатель находился на значении Y, вы устанавливаете его на X; как вы узнаете, что никто не использует (старое) значение Y? Отчасти связанная с этим проблема заключается в том, что вы могли хранить вещи в X, которые другой поток ожидает найти. Без синхронизации другой может увидеть новое значение указателя, однако то, на что он указывает, может быть еще не актуальным.

2 голосов
/ 03 августа 2020

Обычный глобальный char *ptr должен не считаться atomi c. Иногда это может работать, особенно с отключенной оптимизацией, но вы можете заставить компилятор сделать безопасный и эффективный оптимизированный asm за счет использования современных языковых функций, чтобы сообщить ему, что вам нужна атомарность.

Используйте C11 stdatomic.h или GNU C __atomic builtins . И посмотрите Почему целочисленное присваивание естественно выровненной переменной atomi c на x86? - да, базовые операции asm выполняются atomi c «бесплатно», но вам нужно управлять генератором кода компилятора чтобы получить нормальное поведение для многопоточности.

См. также LWN: Кто боится большого плохого оптимизирующего компилятора? - странные эффекты использования простых переменных включают в себя несколько действительно плохих хорошо известных вещей, но также более непонятные вещи, такие как придуманные загрузки, чтение переменной более одного раза, если компилятор решает оптимизировать локальный tmp и дважды загружать общую переменную вместо загрузки ее в регистр. Использование барьеров компилятора asm("" ::: "memory") может быть недостаточным для преодоления этого, в зависимости от того, где вы их поместили.

Поэтому используйте правильные хранилища и загрузки atomi c, которые сообщают компилятору, что вы хотите: Обычно для их чтения вам также следует использовать atomi c load.

#include <stdatomic.h>            // C11 way
_Atomic char *c11_shared_var;     // all access to this is atomic, functions needed only if you want weaker ordering

void foo(){
   atomic_store_explicit(&c11_shared_var, newval, memory_order_relaxed);
}
char *plain_shared_var;       // GNU C
// This is a plain C var.  Only specific accesses to it are atomic; be careful!

void foo() {
   __atomic_store_n(&plain_shared_var, newval, __ATOMIC_RELAXED);
}

Использование __atomic_store_n в простой переменной - это функциональность, которую предоставляет C ++ 20 atomic_ref. Если несколько потоков обращаются к переменной в течение всего времени, в течение которого она должна существовать, вы также можете просто использовать C11 stdatomi c, потому что каждый доступ должен быть atomi c (не оптимизирован в регистр или что-то еще). Если вы хотите, чтобы компилятор загрузился один раз и повторно использовал это значение, сделайте char *tmp = c11_shared_var; (или atomic_load_explicit, если вы хотите получить только вместо seq_cst; дешевле на нескольких ISA, отличных от x86).

Помимо отсутствия разрыва (атомарность загрузки или сохранения asm), другие ключевые части _Atomic foo *:

  • Компилятор будет Предположим, что другие потоки могли изменить содержимое памяти (например, volatile фактически подразумевает), в противном случае предположение об отсутствии UB-гонки данных позволит компилятору поднимать нагрузки из циклов. Без этого устранение мертвого хранилища могло бы сделать только одно хранилище в конце al oop, не обновляя значение несколько раз.

    На практике обычно люди кусаются на стороне чтения, см. Многопоточная программа застряла в оптимизированном режиме, но нормально работает в -O0 - например, while(!flag){} становится if(!flag) infinite_loop; с включенной оптимизацией.

  • Заказ по другой код. например, вы можете использовать memory_order_release, чтобы убедиться, что другие потоки, которые видят обновление указателя, также видят все изменения в данных, на которые указывает. (На x86 это так же просто, как упорядочивание во время компиляции, никаких дополнительных барьеров для получения / выпуска не требуется, только для seq_cst. По возможности избегайте seq_cst; mfence или lock ed операции выполняются медленно.)

  • Гарантия , что хранилище будет компилироваться в одну инструкцию asm. Вы бы зависели от этого. На практике это действительно происходит с разумными компиляторами, хотя вполне возможно, что компилятор может решить использовать rep movsb для копирования нескольких смежных указателей, и что на какой-то машине где-то может быть микрокодированная реализация, которая делает некоторые хранилища меньше 8 байтов.

    (Этот режим сбоя маловероятен; ядро ​​Linux полагается на volatile компиляцию загрузки / сохранения в одну инструкцию с G CC / clang для своих внутренних встроенных функций. Но если вы просто использовали asm("" ::: "memory"), чтобы убедиться, что хранилище произошло с переменной, отличной от volatile, есть шанс.)

Кроме того, что-то вроде ptr++ будет компилироваться в atomi c RMW операция как lock add qword [mem], 4, а не отдельная загрузка и сохранение, как volatile. (См. Может ли num ++ быть atomi c вместо int num? для получения дополнительной информации об atomi c RMW). Избегайте этого, если он вам не нужен, он будет медленнее. например, atomic_store_explicit(&ptr, ptr + 1, mo_release); - загрузки seq_cst дешевы на x86-64, а хранилища seq_cst - нет.

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

На практике x86-64 ABI действительно имеют alignof(void*) = 8 поэтому все объекты-указатели должны быть выровнены естественным образом (за исключением структуры __attribute__((packed)), которая нарушает ABI, поэтому вы можете использовать на них __atomic_store_n. Она должна компилироваться в соответствии с вашими потребностями (обычное хранилище, без накладных расходов) и соответствовать Требования asm должны быть atomi c.

См. также Когда использовать volatile с многопоточностью? - вы можете свернуть свои собственные атомики с volatile и барьерами памяти asm, но не Т. е. Ядро Linux делает это, но это требует больших усилий и практически никакой выгоды, особенно для программы пользовательского пространства.

Дополнительное примечание: часто повторяющееся заблуждение состоит в том, что volatile или _Atomic необходимы, чтобы избежать чтения устаревших значений из кеша . Это не случай.

Все машины, на которых выполняются потоки C11 на нескольких ядрах, имеют согласованные кеши, не нуждающиеся в явных sh инструкциях читателя или записывающего устройства. Обычные инструкции загрузки или сохранения, например x86 mov. Ключ состоит в том, чтобы не позволять компилятору сохранять значения разделяемой переменной в регистрах CPU (которые являются частными для потоков). Обычно он может выполнить эту оптимизацию из-за предположения об отсутствии неопределенного поведения гонки данных. Регистры - это не то же самое, что кэш ЦП L1d; управление тем, что находится в регистрах по сравнению с памятью, осуществляется компилятором, в то время как оборудование синхронизирует кеш c. См. Когда использовать volatile с многопоточностью? для получения дополнительных сведений о том, почему когерентных кешей достаточно, чтобы volatile работал как memory_order_relaxed.

См. Программа многопоточности застряла в оптимизированной режим, но обычно работает в -O0 для примера.

0 голосов
/ 03 августа 2020

«Atomi c» рассматривается как это квантовое состояние, в котором что-то может быть одновременно atomi c и не atomi c, потому что «возможно», что «некоторые машины» «где-то» могут не "напишите" определенное значение "атомарно. Может быть.

Это не так. Атомарность имеет очень специфическое значение c и решает очень специфичную проблему c: потоки, которые ОС вытесняют, чтобы запланировать другой поток вместо него в этом ядре. И вы не можете остановить поток от выполнения инструкции промежуточной сборки.

Это означает, что любая отдельная инструкция сборки является "atomi c" по определению. А поскольку у вас есть инструкции по перемещению реестра, любая копия размером с регистр по определению является atomi c. Это означает, что 32-разрядное целое число на 32-разрядном процессоре и 64-разрядное целое число на 64-разрядном процессоре - все это atomi c - и, конечно, это включает указатели (игнорируйте всех людей, которые скажут вам " некоторые архитектуры "имеют указатели" разного размера ", чем регистры, чего не было с 386 г.).

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

...