В настоящее время изучаю атомарные операции в C/C++ с использованием GCC и обнаружил, что естественно выровненные глобальные переменные в памяти имеют атомарное чтение и запись.
Тем не менее, я пытался использовать побитовую И глобальную переменную и заметил, что она сводится к последовательности чтения-изменения-записи, которая вызывает проблемы, если с этим значением байта работает несколько потоков.
После некоторых исследований я остановился на этих двух примерах:
Пример C - расширение GCC __sync_fetch_and_and
#include <stdio.h>
#include <stdint.h>
uint8_t byteC = 0xFF;
int main() {
__sync_fetch_and_and(&byteC, 0xF0);
printf("Value of byteC: 0x%X\n", byteC);
return 0;
}
Пример C++ - C++11 с использованием атомарного fetch_and
#include <iostream>
#include <atomic>
std::atomic<uint8_t> byteCpp(0xFF);
int main() {
byteCpp.fetch_and(0xF0);
std::cout << "Value of byteCpp: 0x" << std::hex << static_cast<int>(byteCpp.load()) << std::endl;
return 0;
}
Далее следуют другие примеры, но они кажутся менее интуитивными и более затратными в вычислительном отношении.
Использование pthread_mutex_lock
uint8_t byte = 0xFF;
pthread_mutex_t byte_mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_lock(&byte_mutex);
byte &= 0xF0;
pthread_mutex_unlock(&byte_mutex);
Использование мьютекса lock_guard
#include <mutex>
uint8_t byte;
std::mutex byte_mutex;
void atomic_and() {
std::lock_guard<std::mutex> lock(byte_mutex);
byte &= 0xF0;
}
Использование compare_exchange_weak
std::atomic<uint8_t> byte;
void atomic_and() {
uint8_t old_val, new_val;
do {
old_val = byte.load();
new_val = old_val & 0xF0;
} while (!byte.compare_exchange_weak(old_val, new_val));
}
Вопрос
Каков наилучший атомарный метод для последовательности чтения-изменения-записи в многопоточной программе C/C++?
Кажется, вы задаете два разных вопроса: один для C, а другой для C++. Вероятно, в каждом случае ответ будет разным. Выберите, о каком языке вы спрашиваете, и опубликуйте отдельный вопрос для другого языка, если вам действительно нужен ответ для обоих.
@JesperJuhl: естественно выровненные глобальные переменные в памяти имеют атомарное чтение и запись - на ассемблере да для малой степени 2 размера на большинстве ISA, например x86 . Но это не язык ассемблера, поэтому ваше возражение справедливо. «Атомарность» в C и C++ также означает запрет оптимизатору удалять или переупорядочивать операции. Кроме того, вы определенно не получите атомарность RMW бесплатно! Атомарная чистая загрузка, а затем последующее атомарное сохранение другого значения — это совсем не атомарный RMW, поэтому в x86 есть такие инструкции, как lock and byte [mem], reg.
Что такое C/C++, почему это не C++/C?
Это правда или нет? Intel гарантирует, что одна выровненная 32-битная запись в память будет происходить атомарно: т. е. если вы записываете в память целочисленное значение, вам не нужно беспокоиться о том, что другой поток увидит некоторые биты, но не все.
__sync_fetch_and_and является встроенным компилятором, поэтому часть вопроса касается не C, а скорее gcc.
@JesperJuhl: Возможно, ОП имеет в виду чтение ветки комментариев к Влияет ли значение `std::memory_ordering` как на переупорядочение компилятора, так и на аппаратные инструкции для атомарных объектов? вчера, где тот факт, что доступ к простым неатомарным переменным компилируется в те же инструкции asm (например, x86 mov), что и relaxed atomics. Это не означает, что простые переменные C являются атомарными в каком-либо смысле. См. LWN: Кто боится большого плохого оптимизирующего компилятора? re: подводные камни простых переменных в ядре Linux.
С11 _Atomic. 🤷
@vengy - одна запись происходит атомарно, если это происходит. Но для неатомарной переменной компилятор может сохранить значение в регистре и, возможно, записать его позже. Для операции AND вы просто потеряны, так как это и чтение, и запись. Почему бы не довериться std::atomic<uint8_t> использованию наилучшего способа выполнения операции?
@BoP Спасибо. Я буду придерживаться std::atomic, так как это наименее запутанно для меня. На самом деле я думал, что запись значения в глобальную переменную, такую как byte = 0, будет атомарной операцией.
@vengy: «Гарантия Intel заключается в том, что одна выровненная 32-битная запись в память будет происходить атомарно»: гарантировал ли вам компилятор, что если вы присвоите значение 32-битному объекту в исходном коде C, он реализует это с одной выровненной 32-битной памятью писать?
Если вы действительно хотели использовать встроенные функции GNU C, не используйте устаревшие встроенные функции __sync. Используйте __atomic_fetch_and(&byte, 0xf0, __ATOMIC_RELAXED) или что-то еще (gcc.gnu.org/onlinedocs/gcc/_005f_005fatomic-Builtins.html), которые также обеспечивают атомарную чистую загрузку и атомарное чистое хранилище, вместо того, чтобы взломать *(volatile char*), чтобы принудительно загрузить или сохранить произойдет.
Кроме того, x86 — не единственная архитектура, которую поддерживает C++.





[Я] обнаружил, что естественно выровненные глобальные переменные в памяти имеют атомарное чтение и запись.
Это неверно в смысле C/C++, только в смысле x86_64. Это правда, что любые выровненные загрузки и сохранения на x86_64 являются атомарными, но это неверно для абстрактной машины. Одновременная запись в неатомарный бит памяти — это всегда гонка данных, и дезинсекторы потоков могут поймать ошибку, даже если архитектура теоретически делает это безопасным.
Кроме того, лучший способ атомарного выполнения byte &= 0xf0 очень похож на C и C++:
// C++
#include <atomic>
std::atomic_uint8_t byte; // or std::atomic<std::uint8_t>
// ...
std::uint8_t old = byte.fetch_and(0xf0); /* optionally specify memory order */
// or
std::uint8_t old = std::atomic_fetch_and(&byte, 0xf0);
// C (no compiler extensions/intrinsics needed)
#include <stdatomic.h>
atomic_uint8_t byte; // or _Atomic uint8_t
// ...
uint8_t old = atomic_fetch_and(&byte, 0xf0); /* optionally atomic_fetch_and_explicit */
Другие методы (потоки POSIX, std::mutex, compare_exchange повторные циклы) почти наверняка хуже встроенного способа в виде fetch_and функций. Если в архитектуре не предусмотрена инструкция атомарной выборки-И, то следует выбрать тот способ, который лучше всего подходит. Это не то, о чем вам нужно беспокоиться.
Спасибо @PeterCordes за то, что поделились этими ссылками.
Правда в сборке на большинстве ISA, не только на x86-64. Вот почему std::atomic<T>.load(relaxed)/.store(relaxed)` просто компилируется в простые инструкции загрузки и сохранения, те же самые, которые компиляторы используют для простых переменных, для T шириной регистра или меньше на большинстве ISA. Но да, совершенно ошибочное рассуждение, будучи атомарным в C или C++, также означает, что оптимизатору не нужны руки. См. LWN: Кто боится большого плохого оптимизирующего компилятора? re: подводные камни простых переменных в ядре Linux (где они используют volatile с GCC и известными типами и asm для более строгого упорядочения).
Коллега спросил меня о чтении/записи глобальной переменной в C++, поэтому моей первоначальной реакцией было использование std::atomic, но другие люди сказали, что присваивание int выровненной переменной было атомарным. В любом случае, спасибо за все ответы. Мне нужно многому научиться...
«естественно выровненные глобальные переменные в памяти имеют атомарное чтение и запись» - э-э, что? Насколько я знаю, нет.