Ответ 1
Это не компилятор, а конкретная ОС, это специфическая архитектура. Компилятор и ОС входят в него, потому что они - инструменты, с которыми вы работаете, но они не те, которые устанавливают реальные правила. Вот почему стандарт С++ не затронет проблему.
Я никогда в своей жизни не слышал о 64-битной целочисленной записи, которую можно разделить на две 32-битные записи, прерванные на полпути. (Да, это приглашение другим участникам контрпримеры.) В частности, я никогда не слышал о загрузке/хранении процессора, позволяющем прерывать работу с неправильной записью; источник прерывания должен дождаться завершения полного несогласованного доступа.
Чтобы иметь прерывистый блок загрузки/хранения, его состояние должно быть сохранено в стеке... и блок загрузки/хранения - это то, что сохраняет остальную часть состояния процессора в стеке. Это было бы очень сложно, и ошибка была бы подвержена, если бы устройство загрузки/хранения было прерываемым... и все, что вы получили бы, - это задержка на один цикл меньше при реагировании на прерывания, что в лучшем случае измеряется десятками циклов. Полностью не стоит.
Еще в 1997 году мы с коллегой создали шаблон очереди С++, который использовался в многопроцессорной системе. (Каждый процессор имел свою собственную ОС и свою собственную локальную память, поэтому эти очереди были необходимы только для памяти, разделяемой между процессорами.) Мы разработали способ сделать состояние изменения очереди одним целым числом и обработать эту запись как атомную операцию. Кроме того, нам требовалось, чтобы каждый конец очереди (то есть индекс чтения или записи) принадлежал одному и только одному процессору. Тринадцать лет спустя код все еще работает нормально, и у нас даже есть версия, которая обрабатывает несколько считывателей.
Тем не менее, если вы хотите обрабатывать 64-разрядное целое число как атомное, выровняйте поле с 64-битной границей. Зачем беспокоиться?
EDIT: для случая, о котором вы упомянули в своем комментарии, мне нужна дополнительная информация, поэтому позвольте мне привести пример того, что может быть реализовано без специального кода синхронизации.
Предположим, у вас есть 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;
}
Теперь большая во всем этом? Это неблокирование, то есть вы не можете заставить Reader спать до тех пор, пока Writer ничего не напишет. Reader должен выбирать между сидением в цикле спина, ожидающим AnyEvents()
, чтобы вернуть true
, что минимизирует задержку, или может каждый раз усваивать бит, что экономит процессор, но может позволить много событий накапливаться. Так что это лучше, чем ничего, но это не решение всего.
Используя реальные примитивы синхронизации, нужно было бы только обернуть этот код с помощью мьютекса и переменной условия, чтобы сделать его правильной блокировкой: Reader будет спать, пока не будет что-то делать. Поскольку вы использовали атомарные операции с флагами, вы могли бы фактически сохранить время, в течение которого мьютекс заблокирован до минимума: Writer должен был бы только блокировать мьютексы достаточно долго, чтобы отправить условие, а не установить флаг, а читатель только нужно дождаться условия перед вызовом AnyEvents()
(в основном это похоже на случай спящего цикла выше, но с условием ожидания для состояния вместо вызова сна).