Подтвердить что ты не робот

Как объявить и использовать переменную "один писатель, много читателей, один процесс, простой тип"?

У меня действительно простой вопрос. У меня есть простая переменная типа (например, int). У меня есть один процесс, один поток писем, несколько "readonly" потоков. Как объявить переменную?

  • volatile int
  • std::atomic<int>
  • int

Я ожидаю, что когда поток "writer" изменяет значение, все потоки "чтения" должны видеть новое значение ASAP.

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

Я использую однопроцессорную машину Xeon E5 v3. Мне не нужно быть портативным, я запускаю код только на этом сервере, я компилирую его с помощью -march=native -mtune=native. Производительность очень важна, поэтому я не хочу добавлять "накладные расходы синхронизации", если это абсолютно необходимо.


Если я просто использую int и одно значение записи потока возможно, что в другом потоке я не вижу "свежее" значение какое-то время?

4b9b3361

Ответ 1

У меня есть простая переменная типа (например, int). У меня есть один процесс, один поток писем, несколько "readonly" потоков. Как следует объявить переменную?

volatile 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).

Ответ 2

Просто используйте std::atomic.

Не используйте volatile и не используйте его как есть; что не дает необходимой синхронизации. Изменение его в одном потоке и доступ к нему из другого без синхронизации даст поведение undefined.

Ответ 3

Если у вас есть несинхронизированный доступ к переменной, в которой у вас есть один или несколько авторов, ваша программа имеет undefined поведение. Некоторые, как вы должны гарантировать, что, когда происходит запись, никакая другая запись или чтение не может произойти. Это называется synchronization. Как вы достигаете этой синхронизации, зависит от приложения.

Для чего-то вроде этого, когда у нас есть один писатель и несколько читателей, и они используют 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 (С++ 14) или std::shared_mutex (С++ 17), который позволит нескольким читателям обмениваться мьютексом, но когда вам нужно писать, вы все равно можете смотреть мьютексы и записывать данные.

Вы не хотите использовать volatile для управления синхронизацией как jalf говорится в этот ответ:

Для потокобезопасного доступа к общим данным нам нужна гарантия:

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

volatile гарантирует первую точку. Это также гарантирует, что нет переупорядочение происходит между различными volatile чтением/записью. Все volatile Доступ к памяти будет происходить в том порядке, в котором они находятся указано. Это все, что нам нужно, для чего volatile предназначен для: манипулирование регистрами ввода-вывода или аппаратными средствами с отображением памяти, но это не помогите нам в многопоточном коде, где часто находится объект volatileиспользуется только для синхронизации доступа к энергонезависимым данным. Эти обращения все еще могут быть переупорядочены относительно volatile.

Как всегда, если вы измеряете производительность и производительность не хватает, вы можете попробовать другое решение, но обязательно измените и сравните после изменения.

Наконец Herb Sutter имеет отличную презентацию, которую он сделал в С++ и Beyond 2012, называемый Atomic Weapons, который:

Это двухчастный разговор, который охватывает модель памяти С++, как взаимодействуют блокировки, атомы и ограждения и сопоставляются с оборудованием, и многое другое. Хотя речь шла о С++, большая часть этого также применима к Java и .NET, которые имеют похожие модели памяти, но не все возможности С++ (такие как расслабленная атомистика).

Ответ 4

Я получу несколько предыдущих ответов.

Как показано выше, просто использование int или, в конечном счете, volatile int недостаточно по разным причинам (даже с ограничением порядка памяти процессоров Intel.)

Итак, да, вы должны использовать для этого атомарные типы, но вам нужны дополнительные соображения: атомные типы гарантируют когерентный доступ, но если у вас есть проблемы с видимостью, вам нужно указать барьер памяти (порядок памяти.)

Барьеры будут обеспечивать видимость и согласованность между потоками, на Intel и большинств современных архитектурах, это обеспечит синхронизацию кэш-памяти, чтобы обновления отображались для каждого ядра. Проблема в том, что это может быть дорого, если вы недостаточно осторожны.

Возможный порядок памяти:

  • расслабленный: нет специального барьера, обеспечивается только согласованное чтение/запись;
  • последовательная последовательность: максимально возможное ограничение (по умолчанию);
  • приобретать: обеспечить, чтобы никакие нагрузки после того, как текущий был переупорядочен раньше, и добавьте необходимый барьер, чтобы убедиться, что выпущенные магазины видны;
  • потреблять: упрощенная версия получения, которая в основном ограничивает переупорядочивание;
  • release: обеспечить, чтобы все хранилища до того, как они были завершены до того, как текущая, и что записи в памяти сделаны и видны для нагрузок, выполняющих барьер получения.

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

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

Итак, короче говоря, вы должны объявить свой int как атомный и использовать следующий код для хранения и загрузки:

// Your variable
std::atomic<int> v;

// Read
x = v.load(std::memory_order_acquire);

// Write
v.store(x, std::memory_order_release);

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

Я настоятельно рекомендую читать статьи, связанные с релятивистским программированием и RCU, вот несколько интересных ссылок:

Ответ 5

Пусть начинается с int в int. В общем, при использовании на однопроцессорном одноядерном компьютере это должно быть достаточно, если предположить, что размер int такой же или меньше, чем слово ЦП (например, 32bit int на 32-битном процессоре). В этом случае, предполагая правильно выровненные адреса адресных слов (язык высокого уровня должен обеспечить это по умолчанию), операции записи/чтения должны быть атомарными. Это гарантировано Intel, как указано в [1]. Однако в спецификации С++ одновременное чтение и запись из разных потоков - это поведение undefined.

$1,10

6 Две оценки выражений противоречат друг другу, если один из них изменяет расположение памяти (1.7), а другой получает или изменяет одну и ту же ячейку памяти.

Теперь volatile. Это ключевое слово отключает почти каждую оптимизацию. Именно по этой причине он был использован. Например, иногда, когда оптимизация компилятора может прийти к идее, эта переменная, которую вы читаете только в одном потоке, постоянна и просто заменяет ее исходным значением. Это решает такие проблемы. Однако он не обеспечивает доступ к переменной атома. Кроме того, в большинстве случаев это просто не нужно, потому что использование правильных инструментов многопоточности, таких как мьютекс или барьер памяти, будет иметь тот же эффект, что и volatile, как это описано, например, в [2]

Хотя этого может быть достаточно для большинства применений, существуют другие операции, которые не гарантируются как атомарные. Как и приращение, это одно. Это когда приходит std::atomic. Он имеет эти операции, определенные здесь, например, для упомянутых приращений в [3]. Он также хорошо определен при чтении и записи из разных потоков [4].

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

Подводя итог, создается std::atomic для поддержки доступа из разных потоков, и настоятельно рекомендуется использовать его при многопоточности.

[1] http://www.intel.com/Assets/PDF/manual/253668.pdf см. раздел 8.1.1.

[2] https://www.kernel.org/doc/Documentation/volatile-considered-harmful.txt

[3] http://en.cppreference.com/w/cpp/atomic/atomic/operator_arith

[4] http://en.cppreference.com/w/cpp/atomic/atomic

[5] Являются ли чтения и записи С++ для int Atomic?

Ответ 6

К сожалению, это зависит.

Когда переменная читается и записывается в несколько потоков, может быть 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> будут показывать предыдущие допустимые значения x. Используя volatile и atomic, увеличьте порядок значений.

Смотрите барьеры kernel.org

Ответ 7

Вот моя попытка щедрости:  - a. Общий ответ, уже приведенный выше, говорит "использовать атомику". Это правильный ответ. неустойчивого недостаточно. -a. Если вам не нравится ответ, и вы находитесь на Intel, и вы правильно выровняли int, и вам нравятся неуправляемые решения, вы можете избавиться от простой волатильности, используя сторонних поставщиков памяти Intel.

Ответ 8

Другие ответы, которые говорят использовать atomic, а не volatile, верны, когда важна переносимость. Если вы задаете этот вопрос, и его хороший вопрос, это практический ответ для вас, а не: "Но если стандартная библиотека не предоставляет его, вы можете самостоятельно реализовать незащищенную, безжизненную структуру данных". Тем не менее, если стандартная библиотека не предоставляет такой возможности, вы можете самостоятельно реализовать незащищенную структуру данных, которая работает на конкретном компиляторе и конкретной архитектуре при условии, что существует только один сценарий. (Кроме того, кто-то должен реализовать эти атомарные примитивы в стандартной библиотеке.) Если я ошибаюсь, я уверен, что кто-то сообщит мне.

Если вам абсолютно необходим алгоритм, гарантированный блокировка на всех платформах, вы можете создать его с помощью atomic_flag. Если даже этого не достаточно, и вам нужно перевернуть свою собственную структуру данных, вы можете это сделать.

Поскольку существует только один поток писем, ваш процессор может гарантировать, что определенные операции с вашими данными будут работать атомарно, даже если вы используете обычные обращения вместо блокировок или даже сравнения и свопы. Это небезопасно в соответствии с языковым стандартом, потому что С++ должен работать на архитектурах, где это не так, но он может быть безопасным, например, на процессоре x86, если вы гарантируете, что обновление переменной youre вписывается в одну строку кэша, не имеет ничего общего с другим, и вы можете обеспечить это с помощью нестандартных расширений, таких как __attribute__ (( aligned (x) )).

Аналогично, ваш компилятор может предоставить некоторые гарантии: g++, в частности, дает гарантии о том, что компилятор не предполагает, что память, на которую ссылается volatile*, не изменилась, если текущий поток не смог ее изменить. Он будет перечитывать переменную из памяти каждый раз, когда вы разыгрываете ее. Этого недостаточно для обеспечения безопасности потоков, но это может быть удобно, если другой поток обновляет переменную.

Реальный пример может быть: поток писателя поддерживает какой-то указатель (в собственной строке кеша), который указывает на последовательное представление структуры данных, которое останется действительным во всех будущих обновлениях. Он обновляет свои данные с помощью шаблона RCU, убедившись, что после обновления своей копии данных будет использоваться операция выпуска (реализована с учетом архитектуры), и перед тем, как сделать указатель на эти данные глобально видимыми, чтобы любой другой поток, который видит обновленный указатель также сможет увидеть обновленные данные. Затем читатель создает локальную копию (не volatile) текущего значения указателя, получая представление о данных, которые останутся действительными даже после того, как поток писателя снова будет обновлен и будет работать с этим. Вы хотите использовать volatile в единственной переменной, которая уведомляет читателей об обновлениях, чтобы они могли видеть эти обновления, даже если компилятор "знает" ваш поток не мог его изменить. В этих рамках общие данные просто должны быть постоянными, а читатели будут использовать шаблон RCU. Это один из двух способов, которые Ive видел volatile быть полезным в реальном мире (другое существо, когда вы не хотите оптимизировать свою петлю времени).

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

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

Ответ 9

TL; DR: используйте std::atomic<int> с мьютексом вокруг него, если вы читаете несколько раз.

В зависимости от того, насколько сильны гарантии.

Первый volatile - подсказка компилятора, и вы не должны рассчитывать на то, что он делает что-то полезное.

Если вы используете int, вы можете страдать за псевдонимы памяти. Скажем, у вас есть что-то вроде

struct {
  int x;
  bool q;
}

В зависимости от того, как это выровнено в памяти и точной реализации CPU и памяти, возможно, что запись в q фактически перезапишет x, когда страница будет скопирована из кэша процессора обратно в ram. Поэтому, если вы не знаете, сколько выделять вокруг вашего int, это не гарантирует, что ваш писатель сможет писать, не будучи перезаписанным каким-либо другим потоком. Кроме того, даже если вы пишете, вы будете зависеть от процессора для перезагрузки данных в кеш других ядер, поэтому нет гарантии, что ваш другой поток увидит новое значение.

std::atomic<int> в основном гарантирует, что вы всегда будете выделять достаточную память, правильно выровненную так, чтобы вы не страдали от сглаживания. В зависимости от запрошенного порядка памяти вы также отключите кучу оптимизаций, таких как кеширование, поэтому все будет работать немного медленнее.

Это все еще не означает, что если вы будете читать var несколько раз, вы получите значение. Единственный способ сделать это - поместить мьютекс вокруг него, чтобы заблокировать автора от его изменения.

Еще лучше найти библиотеку, которая уже решает проблему, которая у вас есть, и она уже была проверена другими, чтобы убедиться, что она работает хорошо.