Какие варианты использования memory_order_relaxed

Модель памяти C ++ имеет ослабленную атомарность, которая не дает никаких гарантий упорядочивания операций с памятью. Кроме примера почтового ящика на C, который я нашел здесь:

http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1525.htm

На основе мотивирующего примера в этой статье:

http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2007/n2153.pdf

Мне было интересно узнать о других вариантах использования этого типа механизма синхронизации.


person mikelong    schedule 06.05.2014    source источник


Ответы (2)


Простой пример, который я часто вижу в своей работе, - это счетчик статистики. Если вы хотите подсчитать, сколько раз происходит событие, но не нуждаетесь в какой-либо синхронизации между потоками, кроме обеспечения безопасного приращения, использование 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.
  [...]
}

В обоих случаях нет необходимости использовать более сильный порядок памяти. На некоторых платформах это может отрицательно сказаться на производительности.

person jacobsa    schedule 12.06.2014
comment
Было бы также целесообразно использовать ослабленный порядок памяти в случаях, когда что-то вычисляется лениво, и вычисление этого более одного раза было бы немного неэффективным, но в остальном безвредным? Если значение будет прочитано миллионы раз, даже небольшое снижение стоимости каждого чтения может с лихвой компенсировать затраты на несколько избыточных вычислений. - person supercat; 01.11.2015
comment
Мне это кажется нормальным, но вы должны быть очень осторожны, чтобы не пытаться синхронизировать, используя значение. Например, если вы вычисляете структуру, а затем пытаетесь использовать std::atomic<Struct*> с std::memory_order_relaxed, у вас будут плохие времена, потому что вы не гарантируете, что другие потоки увидят записи, инициализирующие структуру, до записи, устанавливающей указатель. - person jacobsa; 02.11.2015
comment
Итак, у вас есть писатели, которые атомарно увеличивают счетчик. Но со временем вам захочется где-нибудь прочитать счетчик. Как и PrintStats () в вашем примере. Так это применимо только тогда, когда у вас есть приращения счета, которые не обязательно должны распространяться немедленно? Когда вы читаете счетчик с помощью std :: memory_order_relaxed, возможно ли, что вы читаете устаревший g_event_count, или нет? - person user643011; 10.10.2017
comment
Я нашел ответ на свой вопрос: единственный способ гарантировать, что у вас есть последнее значение, - это использовать операцию чтения-изменения-записи, такую ​​как exchange (), compare_exchange_strong () или fetch_add () stackoverflow.com/a/8833218/643011 - person user643011; 10.10.2017
comment
Если вы прочитаете значение, вы гарантированно увидите все обновления до самой последней операции синхронизации. Например, если поток A обновляет счетчик, затем разблокирует мьютекс, который принимает поток B, а затем читает счетчик, поток B увидит запись потока A. (Это совместимо с вашей ссылкой, потому что доступ к мьютексу подобен операции чтения-изменения-записи.) В отсутствие такого синхронизирующего события не существует такой вещи, как последнее значение, потому что без такого события невозможно доказать что у вас устаревшее значение. Запись и чтение происходят одновременно. - person jacobsa; 11.10.2017
comment
поскольку нет синхронизации между потоками, вызывающими HandleEvent() и PrintStats(). Объявление static size_t g_event_count_ будет иметь тот же эффект, не так ли? - person HCSF; 05.10.2019
comment
Нет, это приведет к неопределенному поведению из-за гонки данных. Вероятный результат, основанный на том, что компиляторы кода на самом деле сгенерируют, - это потерянные приращения, но теоретически может случиться все, что угодно. - person jacobsa; 06.10.2019
comment
@jacobsa Я вижу, что вы указываете - поскольку кодировщик должен уважать то, что упоминается в стандарте, но не конкретную архитектуру (например, двоичный файл, созданный для x86-64, должен быть одинаковым для всего ‹= 64-битного с или без std::memory_order_relaxed, но он зависит от архитектуры) . - person HCSF; 07.10.2019
comment
@HCSF: если вы пишете код сборки, вы можете понять, что делает архитектура, но не то, что компилятор пишет сборку за вас. Нет такой вещи, как «безобидная гонка за данными». И в этом случае, даже если забыть о гонке данных, вы все равно потеряете приращения: компилятор может сгенерировать наивную нагрузку, добавить 1, сохранить, что не является атомарным. - person jacobsa; 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();
}
person gatis paeglis    schedule 27.08.2018