“псевдоатомарные” операции в C++

Таким образом, я знаю, что ничто не является атомарным в 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 (или безотносительно двух значений, которые я принял решение присвоить)?

12
задан curiousguy 29 November 2018 в 18:22
поделиться

5 ответов

Если ваша реализация C++ предоставляет библиотеку атомарных операций, указанную в n2145 или в каком-то ее варианте, вы, предположительно, можете полагаться на нее. В противном случае вы не можете полагаться на "что-либо" об атомарности на уровне языка, поскольку многозадачность любого рода (и, следовательно, атомарность, которая имеет дело с многозадачностью) не определена существующим стандартом C++.

0
ответ дан 2 December 2019 в 07:02
поделиться

Volatile в C ++ не играет той же роли, что в Java. Как сказал Стив, все случаи являются неопределенным поведением. Некоторые случаи могут быть приемлемыми для компилятора, для данной архитектуры процессора и для многопоточной системы, но переключение флагов оптимизации может заставить вашу программу вести себя иначе, поскольку компиляторы C ++ 03 не знают о потоках.

C ++ 0x определяет правила, позволяющие избежать состояния гонки, и операции, которые помогут вам справиться с этим, но, насколько мне известно, еще не существует компилятора, реализующего все части стандарта, относящиеся к этой теме.

0
ответ дан 2 December 2019 в 07:02
поделиться

В стандартном 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 по этой и другим причинам. Но если вы играете в игры с упаковкой структуры, вы, вероятно, можете спровоцировать пример.

Итак: практически любая реализация даст вам необходимые гарантии, но может сделать это довольно сложным способом.

В общем, я обнаружил, что не стоит полагаться на специфичные для платформы гарантии доступа к памяти, которые я не полностью понимаю, во избежание мьютексов. Используйте мьютекс, а если это слишком медленно, используйте высококачественную структуру без блокировок (или реализуйте ее дизайн), написанную кем-то, кто действительно знает архитектуру и компилятор. Вероятно, это будет правильно, и при условии правильности, вероятно, превзойдет все, что я изобретаю сам.

14
ответ дан 2 December 2019 в 07:02
поделиться

Вообще, это очень, очень плохая идея полагаться на это, так как в итоге могут произойти плохие вещи, и только в некоторых архитектурах. Лучшим решением будет использование гарантированного атомарного API, например, Windows Interlocked api.

1
ответ дан 2 December 2019 в 07:02
поделиться

В большинстве ответов правильно рассматриваются проблемы с упорядочением памяти ЦП, с которыми вы столкнетесь, но ни в одном из них не было подробно описано, как компилятор может помешать вашим намерениям, переупорядочивая код таким образом, чтобы нарушить ваши предположения.

Рассмотрим пример, взятый из этого сообщения :

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 взаимосвязь между потоками ясна.

6
ответ дан 2 December 2019 в 07:02
поделиться
Другие вопросы по тегам:

Похожие вопросы: