У меня действительно простой вопрос. У меня есть простая переменная типа (например, int). У меня есть один процесс, один поток писателя, несколько потоков «только для чтения». Как мне объявить переменную?
volatile int
std::atomic<int>
int
Я ожидаю, что когда поток «писателя» изменяет значение, все потоки «читателя» должны видеть новое значение как можно скорее.
Можно считывать и записывать переменные одновременно, но я ожидаю, что читатель получит либо старое, либо новое значение, а не какое-то «промежуточное» значение.
Я использую однопроцессорную машину Xeon E5 v3. Мне не нужно быть переносимым, я запускаю код только на этом сервере, я компилирую с -march=native -mtune=native
, Производительность очень важна, поэтому я не хочу добавлять «накладные расходы на синхронизацию», если это абсолютно не требуется.
Если я просто использую int
и один поток записывает значение, возможно ли, что в другом потоке я не вижу «свежее» значение некоторое время?
У меня есть простая переменная типа (например, int).
У меня есть один процесс, один поток писателя, несколько потоков «только для чтения». Как
я должен объявить переменную?изменчивый int
станд :: атомное
ИНТ
Используйте std :: atomic с memory_order_relaxed для хранения и загрузки
Это быстро и, исходя из вашего описания вашей проблемы, безопасно. Например.
void func_fast()
{
std::atomic<int> a;
a.store(1, std::memory_order_relaxed);
}
Компилируется в:
func_fast():
movl $1, -24(%rsp)
ret
Это предполагает, что вам не нужно гарантировать, что любые другие данные будут видны как записанные до обновления целого числа, и поэтому более медленная и более сложная синхронизация не требуется.
Если вы используете атомарный наивно, как это:
void func_slow()
{
std::atomic<int> b;
b = 1;
}
Вы получаете инструкцию MFENCE без спецификации memory_order *, которая значительно медленнее (на 100 циклов больше по сравнению с 1 или 2 для чистого MOV).
func_slow():
movl $1, -24(%rsp)
mfence
ret
Увидеть http://goo.gl/svPpUa
(Интересно, что для Intel использование memory_order_release и _acquire для этого кода приводит к одному и тому же языку ассемблера. Intel гарантирует, что запись и чтение происходят по порядку при использовании стандартной инструкции MOV).
Просто используйте std::atomic
,
Не использовать volatile
и не используйте его как есть; это не дает необходимой синхронизации. Изменение его в одном потоке и доступ к нему из другого без синхронизации даст неопределенное поведение.
Если у вас есть несинхронизированный доступ к переменной, где у вас есть один или несколько пишущих, то ваша программа имеет неопределенное поведение. Некоторым образом вы должны гарантировать, что во время записи никакая другая запись или чтение не может произойти. Это называется синхронизация. Как вы достигнете этой синхронизации, зависит от приложения.
Для чего-то вроде этого, где у нас есть один писатель и несколько читателей, и мы используем TriviallyCopyable тип данных затем std::atomic<>
буду работать. Атомная переменная гарантирует, что только один поток может получить доступ к переменной одновременно.
Если у вас нет типа TriviallyCopyable или вы не хотите использовать std::atomic
Вы также можете использовать обычный std::mutex
и std::lock_guard
контролировать доступ
{ // enter locking scope
std::lock_guard lock(mutx); // create lock guard which locks the mutex
some_variable = some_value; // do work
} // end scope lock is destroyed and mutx is released
При таком подходе важно помнить, что вы хотите сохранить // do work
Секция настолько коротка, насколько это возможно, так как пока мьютекс заблокирован, никакой другой поток не сможет войти в эту секцию.
Другой вариант будет использовать std::shared_timed_mutex
(C ++ 14) или std::shared_mutex
(C ++ 17), который позволит нескольким читателям совместно использовать мьютекс, но когда вам нужно написать, вы все равно можете посмотреть мьютекс и записать данные.
Вы не хотите использовать volatile
контролировать синхронизацию как jalf говорится в этот ответ:
Для поточно-ориентированного доступа к общим данным нам нужна гарантия того, что:
- чтение / запись действительно происходит (что компилятор не просто сохранит значение в регистре, а отложит обновление основной памяти до
много позже)- что переупорядочение не происходит. Предположим, что мы используем
volatile
переменная как флаг, чтобы указать, готовы ли некоторые данные к
читать. В нашем коде мы просто устанавливаем флаг после подготовки данных, поэтому
все выглядит хорошо. Но что, если инструкции переупорядочены так флаг
устанавливается первым?
volatile
действительно гарантирует первый пункт. Это также гарантирует, что нет
переупорядочение происходит между различнымиvolatile
чтения / записи. Все
volatile
доступ к памяти будет происходить в том порядке, в котором они
указано. Это все, что нам нужно для чегоvolatile
предназначен для:
манипулирование регистрами ввода / вывода или отображением памяти, но это не так
помочь нам в многопоточном коде, гдеvolatile
объект часто
используется только для синхронизации доступа к энергонезависимым данным. Эти доступы
все еще можно изменить порядок относительноvolatile
из них.
Как всегда, если вы измеряете производительность, а производительности не хватает, вы можете попробовать другое решение, но обязательно измените и сравните после изменения.
наконец Херб Саттер имеет отличную презентацию, которую он сделал в C ++ и после 2012 года называется Атомное оружие тот:
Этот доклад состоит из двух частей: он посвящен модели памяти C ++, взаимодействию замков, атомарности и ограждений, отображению их на оборудование и т. Д. Несмотря на то, что мы говорим о C ++, большая часть этого также применима к Java и .NET, которые имеют схожие модели памяти, но не все функции C ++ (такие как упрощенная атомика).
Я немного дополню предыдущие ответы.
Как указывалось ранее, просто использование int или, возможно, volatile int недостаточно по разным причинам (даже с ограничением порядка памяти процессоров Intel).
Так что, да, вы должны использовать атомарные типы для этого, но вам нужны дополнительные соображения: атомарные типы гарантируют согласованный доступ, но если у вас есть проблемы с видимостью, вам нужно указать барьер памяти (порядок памяти).
Барьеры обеспечат наглядность и согласованность между потоками в Intel и большинстве современных архитектур, а также синхронизируют кэш, поэтому обновления будут видны для всех ядер. Проблема в том, что это может быть дорого, если вы недостаточно осторожны.
Возможный порядок памяти:
Итак, если вы хотите быть уверены, что обновления переменной видны читателям, вам нужно пометить свое хранилище (по крайней мере) порядком освобождения памяти, а на стороне читателя вам нужен порядок захвата памяти (опять же, в минимум.) В противном случае читатели могут не увидеть фактическую версию целого числа (по крайней мере, он увидит связную версию, то есть старую или новую, но не уродливое сочетание двух).
Конечно, поведение по умолчанию (полная согласованность) также даст вам правильное поведение, но за счет большой синхронизации. Короче говоря, каждый раз, когда вы добавляете барьер, он вызывает синхронизацию кеша, которая обходится почти так же дорого, как и пропадание нескольких кешей (и, следовательно, чтение / запись в основной памяти).
Короче говоря, вы должны объявить свой int как атомарный и использовать следующий код для хранения и загрузки:
// Your variable
std::atomic<int> v;
// Read
x = v.load(std::memory_order_acquire);
// Write
v.store(x, std::memory_order_release);
И просто, чтобы завершить, иногда (и чаще, как вы думаете) вам на самом деле не нужна последовательная согласованность (даже согласованность частичного выпуска / приобретения), поскольку видимость обновлений довольно относительна. Когда речь идет о параллельных операциях, обновления происходят не тогда, когда выполняется запись, а когда другие видят изменения, чтение старого значения, вероятно, не проблема!
Я настоятельно рекомендую прочитать статьи, связанные с релятивистским программированием и RCU, вот несколько интересных ссылок:
Давайте начнем с Int в int
, В целом, при использовании на одноядерном, одноядерном компьютере этого должно быть достаточно, предполагая, что размер int такой же или меньше, чем у слова CPU (например, 32-разрядный тип int на 32-разрядном процессоре). В этом случае при условии правильно выровненных адресов слова адреса (язык высокого уровня должен гарантировать это по умолчанию), операции записи / чтения должны быть атомарными. Это гарантируется Intel, как указано в [1]. Однако в спецификации C ++ одновременное чтение и запись из разных потоков является неопределенным поведением.
$ 1,10
6 Две оценки выражений конфликтуют, если одна из них изменяет ячейку памяти (1.7), а другая обращается или изменяет ту же ячейку памяти.
Сейчас volatile
, Это ключевое слово отключает почти каждую оптимизацию. Это причина, почему он был использован. Например, иногда при оптимизации компилятор может прийти к выводу, что переменная, которую вы читаете только в одном потоке, является константой и просто заменяет ее своим начальным значением. Это решает такие проблемы. Однако он не делает доступ к переменной атомарным. Кроме того, в большинстве случаев это просто не нужно, поскольку использование надлежащих инструментов многопоточности, таких как мьютекс или барьер памяти, само по себе даст тот же эффект, что и volatile, как описано, например, в [2]
Хотя этого может быть достаточно для большинства применений, существуют другие операции, которые не гарантированно являются атомарными. Как приращение является одним. Это когда std::atomic
Он определяет эти операции, как здесь для упомянутых приращений в [3]. Это также хорошо определяется при чтении и записи из разных потоков [4].
Кроме того, как указано в ответах в [5], существует множество других факторов, которые могут (отрицательно) влиять на атомарность операций. От потери когерентности кеша между несколькими ядрами до некоторых деталей оборудования — факторы, которые могут изменить способ выполнения операций.
Подвести итоги, std::atomic
Создан для поддержки доступа из разных потоков, и настоятельно рекомендуется использовать его при многопоточности.
К сожалению это зависит.
Когда переменная читается и записывается в нескольких потоках, возможны 2 ошибки.
1) рвать. Где половина данных предварительно изменяется, а половина данных — после изменения.
2) устаревшие данные. Где считанные данные имеют более старое значение.
int, volatile int и std: atomic все не рвутся.
Устаревшие данные — это другая проблема. Тем не менее, все значения существовали, можно считать правильными.
неустойчивый. Это говорит компилятору ни кэшировать данные, ни переупорядочивать операции вокруг них. Это улучшает согласованность между потоками, гарантируя, что все операции в потоке выполняются либо перед переменной, либо над переменной, либо после.
Это означает, что
volatile int x;
int y;
y =5;
x = 7;
инструкция для x = 7 будет написана после y = 5;
К сожалению, процессор также способен переупорядочивать операции. Это может означать, что другой поток видит x == 7 перед y = 5
std :: atomic x; даст гарантию, что после просмотра x == 7 другой поток увидит y == 5. (Предполагая, что другие темы не изменяют y)
Так что все читает int
, volatile int
, std::atomic<int>
будет показывать предыдущие действительные значения х. С помощью volatile
а также atomic
увеличить порядок значений.
Увидеть барьеры kernel.org
Вот моя попытка щедрости:
— а. Общий ответ, уже приведенный выше, гласит: «использовать атомику». Это правильный ответ. летучих недостаточно.
-a. Если вам не нравится ответ, и вы работаете в Intel, и вы правильно настроили int, и вам нравятся непортативные решения, вы можете покончить с простой изменчивостью, используя строгие гарантии упорядочения памяти Intel.
Другие ответы, которые говорят, чтобы использовать atomic
и не volatile
, являются правильными, когда переносимость имеет значение. Если вы задаете этот вопрос, и это хороший вопрос, это практический ответ для вас, а не: «Но если стандартная библиотека не предоставляет такой возможности, вы можете самостоятельно реализовать структуру данных без блокировки и без ожидания». ! »Тем не менее, если стандартная библиотека не предоставляет ее, вы можете самостоятельно реализовать структуру данных без блокировки, которая работает на конкретном компиляторе и конкретной архитектуре, при условии, что существует только один модуль записи. (Кроме того, кто-то должен внедрить эти атомарные примитивы в стандартную библиотеку.) Если я ошибаюсь, я уверен, что кто-то любезно сообщит мне.
Если вам абсолютно необходим алгоритм, который гарантированно не блокируется на всех платформах, вы можете создать его с atomic_flag
, Если даже этого недостаточно, и вам нужно развернуть собственную структуру данных, вы можете сделать это.
Поскольку существует только один поток записи, ваш ЦП может гарантировать, что определенные операции с вашими данными будут по-прежнему работать атомарно, даже если вы просто используете обычный доступ вместо блокировок или даже сравнения и замены. Это не безопасно в соответствии со стандартом языка, потому что C ++ должен работать на архитектурах, где это не так, но это может быть безопасно, например, на процессоре x86 если вы гарантируете, что переменная, которую вы обновляете, помещается в одну строку кэша, которой она не делится ни с чем другим, и вы могли бы обеспечить это с помощью нестандартных расширений, таких как __attribute__ (( aligned (x) ))
,
Точно так же ваш компилятор может предоставить некоторые гарантии: g++
в частности, дает гарантии того, что компилятор не будет предполагать, что память, на которую ссылается volatile*
не изменился, если текущий поток не мог изменить его. Фактически она будет перечитывать переменную из памяти каждый раз, когда вы ее разыменовываете. Это никоим образом достаточно для обеспечения безопасности потока, но это может быть удобно, если другой поток обновляет переменную.
Примером из реальной жизни может быть: поток записи поддерживает некоторый вид указателя (на своей собственной строке кэша), который указывает на непротиворечивое представление структуры данных, которая останется действительной во всех будущих обновлениях. Он обновляет свои данные с помощью шаблона RCU, следя за тем, чтобы использовать операцию освобождения (реализованную с учетом особенностей архитектуры) после обновления своей копии данных и перед тем, как сделать указатель на эти данные видимым глобально, чтобы любой другой поток, который видит обновленный указатель гарантированно увидит обновленные данные. Читатель затем делает локальную копию (не volatile
) текущего значения указателя, получая представление данных, которые останутся действительными даже после того, как поток записи обновится снова, и будет работать с этим. Вы хотите использовать volatile
в единственной переменной, которая уведомляет читателей об обновлениях, чтобы они могли видеть эти обновления, даже если компилятор «знает», что ваш поток не мог его изменить. В этой структуре общие данные просто должны быть постоянными, и читатели будут использовать шаблон RCU. Это один из двух способов, которые я видел volatile
быть полезным в реальном мире (другое, когда вы не хотите оптимизировать свой цикл синхронизации).
В этой схеме также должен быть какой-то способ, чтобы программа знала, когда никто больше не использует старое представление о структуре данных. Если это число считывателей, то это количество нужно атомарно изменить в одной операции одновременно с чтением указателя (поэтому получение текущего представления структуры данных включает атомарный CAS). Или это может быть периодический тик, когда все потоки гарантированно будут выполнены с данными, с которыми они сейчас работают. Это может быть структура данных поколений, в которой устройство записи вращается через предварительно выделенные буферы.
Также обратите внимание, что многое из того, что может сделать ваша программа, может неявно сериализовать потоки: эти атомарные инструкции аппаратного обеспечения блокируют шину процессора и заставляют другие процессоры ждать, эти ограничения памяти могут остановить ваши потоки, или ваши потоки могут ждать в очереди, чтобы выделить память из кучи.