Таким образом, я знаю, что ничто не является атомарным в C++. Но я пытаюсь выяснить, существуют ли какие-либо "псевдоатомарные" предположения, которые я могу сделать. Причина состоит в том, что я хочу избегать использования взаимных исключений в некоторых простых ситуациях, где мне только нужны очень слабые гарантии.
1) Предположим, что я глобально определил энергозависимый bool b, который первоначально я установил верный. Затем я запускаю поток, который выполняет цикл
while(b) doSomething();
Между тем, в другом потоке, я выполняю b=true.
Я могу предположить, что первый поток продолжит выполняться? Другими словами, если b начинается как верный, и первый поток проверяет значение b в то же время, что и второй поток присваивает b=true, я могу предположить, что первый поток считает значение b как верное? Или действительно ли возможно, что в некоторой промежуточной точке присвоения b=true, значение b могло бы быть считано как ложь?
2) Теперь предположите, что b является первоначально ложным. Затем первый поток выполняется
bool b1=b;
bool b2=b;
if(b1 && !b2) bad();
в то время как второй поток выполняет b=true. Я могу предположить, что плохо () никогда не называется?
3) Что относительно интервала или других встроенных типов: предположите, что у меня есть энергозависимый интервал i, который является, первоначально (говорят) 7, и затем я присваиваю i=7. Я могу предположить, что, когда-либо во время этой операции, от потока, значения буду равен 7?
4) У меня есть энергозависимый интервал i=7, и затем я выполняю меня ++ от некоторого потока, и все другие потоки только читают значение меня. Я могу предположить, что у меня никогда нет значения, ни в каком потоке, за исключением или 7 или 8?
5) У меня есть энергозависимый интервал i от одного потока, я выполняю i=7, и от другого я выполняю i=8. Впоследствии, я, как гарантируют, буду или 7 или 8 (или безотносительно двух значений, которые я принял решение присвоить)?
Если ваша реализация C++ предоставляет библиотеку атомарных операций, указанную в n2145 или в каком-то ее варианте, вы, предположительно, можете полагаться на нее. В противном случае вы не можете полагаться на "что-либо" об атомарности на уровне языка, поскольку многозадачность любого рода (и, следовательно, атомарность, которая имеет дело с многозадачностью) не определена существующим стандартом C++.
Volatile в C ++ не играет той же роли, что в Java. Как сказал Стив, все случаи являются неопределенным поведением. Некоторые случаи могут быть приемлемыми для компилятора, для данной архитектуры процессора и для многопоточной системы, но переключение флагов оптимизации может заставить вашу программу вести себя иначе, поскольку компиляторы C ++ 03 не знают о потоках.
C ++ 0x определяет правила, позволяющие избежать состояния гонки, и операции, которые помогут вам справиться с этим, но, насколько мне известно, еще не существует компилятора, реализующего все части стандарта, относящиеся к этой теме.
В стандартном C ++ нет потоков, и Потоки не могут быть реализованы как библиотека .
Таким образом, в стандарте ничего не говорится о поведении программ, использующих потоки. Вы должны обратить внимание на любые дополнительные гарантии, предоставляемые вашей реализацией потоковой передачи.
Тем не менее, в реализациях потоковой передачи, которые я использовал:
(1) да, вы можете предположить, что нерелевантные значения не записываются в переменные. В противном случае вся модель памяти вылетает из окна.Но будьте осторожны: когда вы говорите «другой поток», никогда не устанавливайте для b
значение false, то есть где угодно и когда-либо. Если это так, то эту запись, возможно, можно было бы переупорядочить, чтобы она происходила во время вашего цикла.
(2) нет, компилятор может изменить порядок присваиваний для b1 и b2, так что b1 может оказаться истинным, а b2 - ложным. В таком простом случае я не знаю, почему он будет переупорядочен, но в более сложных случаях могут быть очень веские причины.
[Edit: ой, к тому времени, когда я дошел до ответа (2), я забыл, что b был изменчивым. Чтения из изменчивой переменной не будут переупорядочены, извините, поэтому да, в типичной реализации потоковой передачи (если такая есть) вы можете предположить, что вы не получите b1 true и b2 false.]
(3) то же, что и 1. volatile
вообще не имеет ничего общего с потоками. Однако это довольно интересно в некоторых реализациях (Windows) и может фактически подразумевать барьеры памяти.
(4) в архитектуре, где записи int
являются атомарными, да, хотя volatile
не имеет к этому никакого отношения. См. Также ...
(5) внимательно проверьте документацию. Скорее всего, да, и снова volatile не имеет значения, потому что почти на всех архитектурах int
записи являются атомарными. Но если int
write не является атомарным, тогда нет (и нет в предыдущем вопросе), даже если он изменчив, вы в принципе могли бы получить другое значение.Однако, учитывая эти значения 7 и 8, мы говорим о довольно странной архитектуре для байта, содержащего соответствующие биты, который должен быть записан в два этапа, но с другими значениями вы могли бы более вероятно получить частичную запись.
В качестве более правдоподобного примера предположим, что по какой-то странной причине у вас есть 16-битное int на платформе, где только 8-битные записи являются атомарными. Странно, но допустимо, и поскольку int
должно быть не менее 16 бит, вы можете понять, как это могло произойти. Предположим далее, что ваше начальное значение равно 255. Тогда приращение может быть юридически реализовано как:
Доступный только для чтения поток, который прерывает увеличивающийся поток между третьим и четвертым этапами, может видеть значение 511. Если записи выполняются в другом порядке, он может увидеть 0.
Несогласованное значение могло быть остается позади навсегда, если один поток записывает 255, другой поток одновременно записывает 256, и записи чередуются. Невозможно на многих архитектурах, но чтобы знать, что этого не произойдет, вам нужно хоть что-то знать об архитектуре. Ничто в стандарте C ++ не запрещает этого, потому что стандарт C ++ говорит о прерывании выполнения сигналом, но в остальном не имеет понятия о том, что выполнение прерывается другой частью программы, и не имеет понятия о параллельном выполнении. Вот почему потоки - это не просто еще одна библиотека - добавление потоков коренным образом меняет модель выполнения C ++.Это требует, чтобы реализация делала все по-другому, как вы в конечном итоге обнаружите, если, например, используете потоки в gcc и забываете указать -pthreads
.
То же самое может произойти на платформе, где записи выровненные int
являются атомарными, но невыровненные записи int
разрешены, а не атомарны. Например, IIRC на x86, невыровненные записи int
не гарантируются атомарно, если они пересекают границу строки кэша.Компиляторы x86 не будут неправильно выравнивать объявленную переменную int
по этой и другим причинам. Но если вы играете в игры с упаковкой структуры, вы, вероятно, можете спровоцировать пример.
Итак: практически любая реализация даст вам необходимые гарантии, но может сделать это довольно сложным способом.
В общем, я обнаружил, что не стоит полагаться на специфичные для платформы гарантии доступа к памяти, которые я не полностью понимаю, во избежание мьютексов. Используйте мьютекс, а если это слишком медленно, используйте высококачественную структуру без блокировок (или реализуйте ее дизайн), написанную кем-то, кто действительно знает архитектуру и компилятор. Вероятно, это будет правильно, и при условии правильности, вероятно, превзойдет все, что я изобретаю сам.
Вообще, это очень, очень плохая идея полагаться на это, так как в итоге могут произойти плохие вещи, и только в некоторых архитектурах. Лучшим решением будет использование гарантированного атомарного API, например, Windows Interlocked api.
В большинстве ответов правильно рассматриваются проблемы с упорядочением памяти ЦП, с которыми вы столкнетесь, но ни в одном из них не было подробно описано, как компилятор может помешать вашим намерениям, переупорядочивая код таким образом, чтобы нарушить ваши предположения.
Рассмотрим пример, взятый из этого сообщения :
volatile int ready;
int message[100];
void foo(int i)
{
message[i/10] = 42;
ready = 1;
}
На -O2
и выше последние версии GCC и Intel C / C ++ (не знаю о VC ++) будут сначала сделайте хранилище до готово
, чтобы его можно было перекрыть с вычислением i / 10
( volatile
вас не спасет!):
leaq _message(%rip), %rax
movl $1, _ready(%rip) ; <-- whoa Nelly!
movq %rsp, %rbp
sarl $2, %edx
subl %edi, %edx
movslq %edx,%rdx
movl $42, (%rax,%rdx,4)
This isn Это не ошибка, это оптимизатор, использующий конвейерную обработку ЦП. Если другой поток ожидает готов
, прежде чем получить доступ к содержимому сообщения
, то у вас неприятная и непонятная гонка.
Используйте барьеры компилятора, чтобы гарантировать соблюдение ваших намерений. Примером, который также использует относительно строгий порядок x86, являются оболочки выпуска / потребления, обнаруженные в очереди Дмитрия Вьюкова Single-Producer Single-Consumer , размещенной здесь :
// load with 'consume' (data-dependent) memory ordering
// NOTE: x86 specific, other platforms may need additional memory barriers
template<typename T>
T load_consume(T const* addr)
{
T v = *const_cast<T const volatile*>(addr);
__asm__ __volatile__ ("" ::: "memory"); // compiler barrier
return v;
}
// store with 'release' memory ordering
// NOTE: x86 specific, other platforms may need additional memory barriers
template<typename T>
void store_release(T* addr, T v)
{
__asm__ __volatile__ ("" ::: "memory"); // compiler barrier
*const_cast<T volatile*>(addr) = v;
}
Я предлагаю, если вы собираетесь рискнуть В области одновременного доступа к памяти используйте библиотеку, которая позаботится об этих деталях за вас. Пока мы все ждем n2145 и std :: atomic
, ознакомьтесь с блоками Thread Building Blocks tbb :: atomic
или грядущим boost :: atomic
.
Помимо правильности, эти библиотеки могут упростить ваш код и прояснить ваши намерения:
// thread 1
std::atomic<int> foo; // or tbb::atomic, boost::atomic, etc
foo.store(1, std::memory_order_release);
// thread 2
int tmp = foo.load(std::memory_order_acquire);
Используя явное упорядочение памяти, foo
взаимосвязь между потоками ясна.