Модель памяти C ++ имеет ослабленную атомарность, которая не дает никаких гарантий упорядочивания операций с памятью. Кроме примера почтового ящика на C, который я нашел здесь:
Простой пример, который я часто вижу в своей работе, - это счетчик статистики. Если вы хотите подсчитать, сколько раз происходит событие, но не нуждаетесь в какой-либо синхронизации между потоками, кроме обеспечения безопасного приращения, использование memory_order_relaxed имеет смысл.
static std::atomic<size_t> g_event_count_;
void HandleEvent() {
// Increment the global count. This operation is safe and correct even
// if there are other threads concurrently running HandleEvent or
// PrintStats.
g_event_count_.fetch_add(1, std::memory_order_relaxed);
[...]
}
void PrintStats() {
// Snapshot the "current" value of the counter. "Current" is in scare
// quotes because the value may change while this function is running.
// But unlike a plain old size_t, reading from std::atomic<size_t> is
// safe.
const size_t event_count =
g_event_count_.load(std::memory_order_relaxed);
// Use event_count in a report.
[...]
}
В обоих случаях нет необходимости использовать более сильный порядок памяти. На некоторых платформах это может отрицательно сказаться на производительности.
personjacobsaschedule12.06.2014
comment
Было бы также целесообразно использовать ослабленный порядок памяти в случаях, когда что-то вычисляется лениво, и вычисление этого более одного раза было бы немного неэффективным, но в остальном безвредным? Если значение будет прочитано миллионы раз, даже небольшое снижение стоимости каждого чтения может с лихвой компенсировать затраты на несколько избыточных вычислений.
- personsupercat; 01.11.2015
comment
Мне это кажется нормальным, но вы должны быть очень осторожны, чтобы не пытаться синхронизировать, используя значение. Например, если вы вычисляете структуру, а затем пытаетесь использовать std::atomic<Struct*> с std::memory_order_relaxed, у вас будут плохие времена, потому что вы не гарантируете, что другие потоки увидят записи, инициализирующие структуру, до записи, устанавливающей указатель.
- personjacobsa; 02.11.2015
comment
Итак, у вас есть писатели, которые атомарно увеличивают счетчик. Но со временем вам захочется где-нибудь прочитать счетчик. Как и PrintStats () в вашем примере. Так это применимо только тогда, когда у вас есть приращения счета, которые не обязательно должны распространяться немедленно? Когда вы читаете счетчик с помощью std :: memory_order_relaxed, возможно ли, что вы читаете устаревший g_event_count, или нет?
- personuser643011; 10.10.2017
comment
Я нашел ответ на свой вопрос: единственный способ гарантировать, что у вас есть последнее значение, - это использовать операцию чтения-изменения-записи, такую как exchange (), compare_exchange_strong () или fetch_add () stackoverflow.com/a/8833218/643011 - personuser643011; 10.10.2017
comment
Если вы прочитаете значение, вы гарантированно увидите все обновления до самой последней операции синхронизации. Например, если поток A обновляет счетчик, затем разблокирует мьютекс, который принимает поток B, а затем читает счетчик, поток B увидит запись потока A. (Это совместимо с вашей ссылкой, потому что доступ к мьютексу подобен операции чтения-изменения-записи.) В отсутствие такого синхронизирующего события не существует такой вещи, как последнее значение, потому что без такого события невозможно доказать что у вас устаревшее значение. Запись и чтение происходят одновременно.
- personjacobsa; 11.10.2017
comment
поскольку нет синхронизации между потоками, вызывающими HandleEvent() и PrintStats(). Объявление static size_t g_event_count_ будет иметь тот же эффект, не так ли?
- personHCSF; 05.10.2019
comment
Нет, это приведет к неопределенному поведению из-за гонки данных. Вероятный результат, основанный на том, что компиляторы кода на самом деле сгенерируют, - это потерянные приращения, но теоретически может случиться все, что угодно.
- personjacobsa; 06.10.2019
comment
@jacobsa Я вижу, что вы указываете - поскольку кодировщик должен уважать то, что упоминается в стандарте, но не конкретную архитектуру (например, двоичный файл, созданный для x86-64, должен быть одинаковым для всего ‹= 64-битного с или без std::memory_order_relaxed, но он зависит от архитектуры) .
- personHCSF; 07.10.2019
comment
@HCSF: если вы пишете код сборки, вы можете понять, что делает архитектура, но не то, что компилятор пишет сборку за вас. Нет такой вещи, как «безобидная гонка за данными». И в этом случае, даже если забыть о гонке данных, вы все равно потеряете приращения: компилятор может сгенерировать наивную нагрузку, добавить 1, сохранить, что не является атомарным.
- personjacobsa; 07.10.2019
Считыватель событий в этом случае может быть подключен к сокету X11, где частота событий зависит от действий пользователя (изменение размера окна, набор текста и т. Д.). И если диспетчер событий потока графического интерфейса пользователя проверяет события через регулярные промежутки времени (например, из-за некоторого таймера событий в пользовательском приложении) мы не хотим без нужды блокировать поток чтения событий, получая блокировку общей очереди событий, которая, как мы знаем, пуста. Мы можем просто проверить, поставлено ли что-нибудь в очередь, используя атомар dataReady. Это также известно как шаблон «Двойная проверка блокировки».
namespace {
std::mutex mutex;
std::atomic_bool dataReady(false);
std::atomic_bool done(false);
std::deque<int> events; // shared event queue, protected by mutex
}
void eventReaderThread()
{
static int eventId = 0;
std::chrono::milliseconds ms(100);
while (true) {
std::this_thread::sleep_for(ms);
mutex.lock();
eventId++; // populate event queue, e.g from pending messgaes on a socket
events.push_back(eventId);
dataReady.store(true, std::memory_order_release);
mutex.unlock();
if (eventId == 10) {
done.store(true, std::memory_order_release);
break;
}
}
}
void guiThread()
{
while (!done.load(std::memory_order_acquire)) {
if (dataReady.load(std::memory_order_acquire)) { // Double-checked locking pattern
mutex.lock();
std::cout << events.front() << std::endl;
events.pop_front();
// If consumer() is called again, and producer() has not added new events yet,
// we will see the value set via this memory_order_relaxed.
// If producer() has added new events, we will see that as well due to normal
// acquire->release.
// relaxed docs say: "guarantee atomicity and modification order consistency"
dataReady.store(false, std::memory_order_relaxed);
mutex.unlock();
}
}
}
int main()
{
std::thread producerThread(eventReaderThread);
std::thread consumerThread(guiThread);
producerThread.join();
consumerThread.join();
}