Ответ 1
Нет, как стандарт C, так и С++ не гарантируют, что операции присваивания будут атомарными. Для этого вам нужны некоторые специфические для реализации вещи - либо что-то в компиляторе или в операционной системе.
Я пишу программу, которая имеет один процесс чтения и записи в разделяемую память, а другой процесс - только чтение. В общей памяти существует такая структура:
struct A{
int a;
int b;
double c;
};
то, что я ожидаю, это прочитать структуру сразу, потому что пока я читаю, другой процесс может изменять содержание структуры. Это может быть достигнуто, если назначение структуры является атомарным, что не прерывается. Вот так:
struct A r = shared_struct;
Итак, является ли назначение struct атомарным в C/С++? Я пробовал искать в Интернете, но не могу найти полезные ответы. Может ли кто-нибудь помочь? Спасибо.
Нет, как стандарт C, так и С++ не гарантируют, что операции присваивания будут атомарными. Для этого вам нужны некоторые специфические для реализации вещи - либо что-то в компиляторе или в операционной системе.
C и С++ поддерживают атомные типы в своих текущих стандартах.
В С++ 11 появилась поддержка атомных типов. Аналогично, C11 представил atomics.
Нет, это не так.
Это на самом деле свойство архитектуры процессора по отношению к макету памяти ударов
Вы можете использовать решение "атомный указатель swap", который может быть сделан атомарным и может использоваться в сценарии без блокировки.
Обязательно пометьте соответствующий общий указатель (переменные) как изменчивый, если важно, чтобы изменения были замечены другими "немедленно" . Этого, в реальной жизни (TM) недостаточно, чтобы гарантировать правильное лечение компилятором. Вместо этого используйте непосредственно против атомных примитивов/встроенных средств, если вы хотите иметь свободную семантику. (см. комментарии и связанные статьи для фона)
Конечно, наоборот, вам нужно будет сделать глубокую копию в соответствующие моменты времени, чтобы сделать обработку на стороне чтения.
Теперь все это быстро становится очень сложным по отношению к управлению памятью, и я предлагаю вам тщательно изучить ваш дизайн и спросить себя серьезно, оправдывают ли все преимущества (воспринимаемые?) эффективность. Почему бы вам не выбрать простую блокировку (считыватель/запись) или воспользоваться реалистичной реализацией общего указателя, которая является потокобезопасной?
Вам нужно атомизировать снимки всех элементов структуры? Или вам просто нужен общий доступ для чтения/записи для отдельных членов отдельно? Последнее намного проще, см. Ниже.
C11 stdatomic и С++ 11 std:: atomic обеспечивают синтаксис для атомных объектов произвольных размеров. Но если они больше 8B или 16B, они не будут блокироваться в обычных системах. (то есть атомная нагрузка, хранение, обмен или CAS будет реализована путем скрытия блокировки и последующего копирования всей структуры).
Если вы просто хотите, чтобы члены пары, возможно, лучше использовать блокировку самостоятельно, а затем получить доступ к членам, вместо того, чтобы компилятор скопировать всю структуру. (Текущие компиляторы не очень хороши для оптимизации странных применений атоматики, подобных этому).
Или добавить уровень косвенности, поэтому есть указатель, который можно легко обновить атомарно, чтобы указать на другой struct
с другим набором значений. Это строительный блок для RCU (Read-Copy-Update) См. также https://lwn.net/Articles/262464/. Есть хорошие реализации библиотеки RCU, поэтому используйте их вместо того, чтобы кататься самостоятельно, если ваш прецедент не намного проще, чем общий случай. Выяснить, когда освобождать старые копии структуры, является одной из трудных частей, потому что вы не можете сделать этого до тех пор, пока последний поток читателя не будет выполнен с ним. И вся суть RCU заключается в том, чтобы сделать путь чтения максимально легким...
В большинстве систем ваша структура составляет 16 байт; достаточно мало, чтобы x86-64 мог загружать или хранить все вещи несколько более эффективно, чем просто делать блокировку. (Но только с lock cmpxchg16b
). Тем не менее, это не совсем глупо использовать атомы C/С++ для этого
общий как для С++ 11, так и для C11:
struct A{
int a;
int b;
double c;
};
В C11 используйте классификатор типа _Atomic
, чтобы сделать атомный тип. Это определитель, например const
или volatile
, поэтому вы можете использовать его практически во всем.
#include <stdatomic.h>
_Atomic struct A shared_struct;
// atomically take a snapshot of the shared state and do something
double read_shared (void) {
struct A tmp = shared_struct; // defaults to memory_order_seq_cst
// or atomic_load_explicit(&shared_struct, &tmp, memory_order_relaxed);
//int t = shared_struct.a; // UNDEFINED BEHAVIOUR
// then do whatever you want with the copy, it a normal struct
if (tmp.a > tmp.b)
tmp.c = -tmp.c;
return tmp.c;
}
// or take tmp by value or pointer as a function arg
// static inline
void update_shared(int a, int b, double c) {
struct A tmp = {a, b, c};
//shared_struct = tmp;
// If you just need atomicity, not ordering, relaxed is much faster for small lock-free objects (no memory barrier)
atomic_store_explicit(&shared_struct, tmp, memory_order_relaxed);
}
Обратите внимание, что доступ к одному члену структуры _Atomic
является undefined. Он не будет соблюдать блокировку и может не быть атомарным. Так что не делайте int i = shared_state.a;
(С++ 11 не компилирует это, но C11 будет).
В С++ 11 это почти то же самое: используйте шаблон std::atomic<T>
.
#include <atomic>
std::atomic<A> shared_struct;
// atomically take a snapshot of the shared state and do something
double read_shared (void) {
A tmp = shared_struct; // defaults to memory_order_seq_cst
// or A tmp = shared_struct.load(std::memory_order_relaxed);
// or atomic_load_explicit(&shared_struct, &tmp, memory_order_relaxed);
//int t = shared_struct.a; // won't compile: no operator.() overload
// then do whatever you want with the copy, it a normal struct
if (tmp.a > tmp.b)
tmp.c = -tmp.c;
return tmp.c;
}
void update_shared(int a, int b, double c) {
struct A tmp{a, b, c};
//shared_struct = tmp;
// If you just need atomicity, not ordering, relaxed is much faster for small lock-free objects (no memory barrier)
shared_struct.store(tmp, std::memory_order_relaxed);
}
Посмотрите в проводнике компилятора Godbolt
Если вам не нужно снимок всей структуры, но вместо этого просто нужно, чтобы каждый член был отдельно атомарным, вы можете просто сделать каждый член атомарным типом. (Как atomic_int
и _Atomic double
или std::atomic<double>
).
struct Amembers {
atomic_int a, b;
#ifdef __cplusplus
std::atomic<double> c;
#else
_Atomic double c;
#endif
} shared_state;
// If these members are used separately, put them in separate cache lines
// instead of in the same struct to avoid false sharing cache-line ping pong.
(Обратите внимание, что C11 stdatomic не гарантируется совместимость с С++ 11 std:: atomic, поэтому не ожидайте, что сможете получить доступ к одной и той же структуре с C или С++.)
В С++ 11 struct-assign для struct с атомарными членами не будет компилироваться, потому что std::atomic
удаляет свой экземпляр-конструктор. (Вы должны загрузить std::atomic<T> shared
в T tmp
, как и в примере всей структуры выше.)
В C11 struct-assign для неатомной структуры с атомарными членами будет компилироваться, но не является атомарным. Стандарт C11 конкретно не указывает на это. Самое лучшее, что я могу найти: n1570: 6.5.16.1 Простое назначение:
В простом присваивании (=) значение правильного операнда преобразуется в тип присваивание и заменяет значение, хранящееся в объекте, указанном слева операнд.
Так как это ничего не говорит об особой обработке атомных членов, следует предположить, что он похож на memcpy
объектов. (За исключением того, что разрешено не обновлять дополнение).
На практике легко получить gcc для генерации asm для структур с атомарным членом, где он копирует неатомно. Особенно с большим атомарным элементом, который является атомарным, но не блокируемым.
Ну, безусловно, не атомный, но компилятор, скорее всего, выдает копию как memcpy() из N байтов, где байты - это размер структуры