Это не зависит от компилятора и ОС, это зависит от архитектуры. Компилятор и операционная система входят в него, потому что это инструменты, с которыми вы работаете, но не те, которые устанавливают настоящие правила. Вот почему стандарт C ++ не касается этой проблемы.
Я никогда в своей жизни не слышал о 64-битной записи целых чисел, которая может быть разделена на две 32-битные записи, прерываемые на полпути. (Да, это приглашение для других опубликовать контрпримеры.) В частности, я никогда не слышал о модуле загрузки / хранения ЦП, позволяющем прерывать неправильную запись; прерывающий источник должен ждать завершения всего смещенного доступа.
Чтобы иметь прерываемый модуль загрузки / хранения, его состояние должно быть сохранено в стеке ... и модуль загрузки / хранения - это то, что сохраняет остальную часть состояния ЦП в стек. Это было бы чрезвычайно сложным и подверженным ошибкам, если бы единица загрузки / хранения была прерываемой ... и все, что вы получили бы, это на один цикл меньшая задержка при реагировании на прерывания, что В лучшем случае измеряется десятками циклов. Совершенно не стоит.
Еще в 1997 году мы с коллегой написали шаблон C ++ Queue, который использовался в многопроцессорной системе. (У каждого процессора была своя собственная ОС и своя собственная локальная память, поэтому эти очереди были нужны только для памяти, разделяемой между процессорами.) Мы разработали способ создания состояния изменения очереди с одной записью целого числа и рассматривали эту запись как атомная операция. Кроме того, мы требовали, чтобы каждый конец очереди (то есть индекс чтения или записи) принадлежал одному и только одному процессору. Тринадцать лет спустя код все еще работает нормально, и у нас даже есть версия, которая обрабатывает несколько читателей.
Тем не менее, если вы хотите рассматривать 64-разрядную целочисленную запись как атомарную, выровняйте поле с 64-разрядной границей. Зачем беспокоиться?
РЕДАКТИРОВАТЬ: Для случая, который вы упомянули в своем комментарии, мне понадобится больше информации, чтобы быть уверенным, поэтому позвольте мне привести пример того, что может быть реализовано без специального кода синхронизации.
Предположим, у вас есть N писателей и один читатель. Вы хотите, чтобы писатели могли сигнализировать о событиях читателю. Сами события не имеют данных; Вы просто хотите подсчитать количество событий, на самом деле.
Объявите структуру для разделяемой памяти, разделяемой между всеми авторами и читателем:
#include <stdint.h>
struct FlagTable
{ uint32_t flag[NWriters];
};
(Сделайте это классом или шаблоном или чем-то еще, как считаете нужным)
Каждый автор должен получить свой индекс и указатель на эту таблицу:
class Writer
{public:
Writer(FlagTable* flags_, size_t index_): flags(flags_), index(index_) {}
void SignalEvent(uint32_t eventCount = 1);
private:
FlagTable* flags;
size_t index;
}
Когда автор хочет сообщить о событии (или нескольких), он обновляет свой флаг:
void Writer::SignalEvent(uint32_t eventCount)
{ // Effectively atomic: only one writer modifies this value, and
// the state changes when the incremented value is written out.
flags->flag[index] += eventCount;
}
Считыватель хранит локальную копию всех значений флага, которые он видел:
class Reader
{public:
Reader(FlagTable* flags_): flags(flags_)
{ for(size_t i = 0; i < NWriters; ++i)
seenFlags[i] = flags->flag[i];
}
bool AnyEvents(void);
uint32_t CountEvents(int writerIndex);
private:
FlagTable* flags;
uint32_t seenFlags[NWriters];
}
Чтобы узнать, произошло ли какое-либо событие, нужно просто посмотреть измененные значения:
bool Reader::AnyEvents(void)
{ for(size_t i = 0; i < NWriters; ++i)
if(seenFlags[i] != flags->flag[i])
return true;
return false;
}
Если что-то произошло, мы можем проверить каждый источник и получить количество событий:
uint32_t Reader::CountEvents(int writerIndex)
{ // Only read a flag once per function call. If you read it twice,
// it may change between reads and then funny stuff happens.
uint32_t newFlag = flags->flag[i];
// Our local copy, though, we can mess with all we want since there
// is only one reader.
uint32_t oldFlag = seenFlags[i];
// Next line atomically changes Reader state, marking the events as counted.
seenFlags[i] = newFlag;
return newFlag - oldFlag;
}
А теперь, во всем этом большая ошибка? Это неблокирование, то есть вы не можете заставить Читатель спать, пока Писатель что-то не напишет. Считыватель должен выбрать между сидением в спин-петле и ожиданием возврата AnyEvents()
, чтобы вернуть true
, что минимизирует задержку, или он может каждый раз немного спать, что экономит процессор, но может позволить нарастить много событий. Так что лучше, чем ничего, но это не решение для всего.
Используя настоящие примитивы синхронизации, нужно всего лишь обернуть этот код мьютексом и переменной условия, чтобы он правильно блокировался: считыватель будет спать до тех пор, пока не будет что-то делать. Так как вы использовали атомарные операции с флагами, вы могли фактически сохранить количество времени, в течение которого мьютекс заблокирован, до минимума: Writer нужно будет только заблокировать мьютекс достаточно долго, чтобы отправить условие, и не устанавливать флаг, а читатель нужно только дождаться условия перед вызовом AnyEvents()
(по сути, это похоже на описанный выше случай цикла ожидания, но с ожиданием условия вместо вызова режима ожидания).