Атомность в С++: миф или реальность

Я читал статью о Программе блокировки в MSDN. В нем говорится:

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

И он дает несколько примеров:

// This write is not atomic because it is not natively aligned.
DWORD* pData = (DWORD*)(pChar + 1);
*pData = 0;

// This is not atomic because it is three separate operations.
++g_globalCounter;

// This write is atomic.
g_alignedGlobal = 0;

// This read is atomic.
DWORD local = g_alignedGlobal;

Я прочитал много ответов и комментариев, говорящих: ничто не гарантировано является атомарным в С++, и это даже не упоминается в стандартах, в SO, и теперь я немного смущен. Я неправильно истолковал статью? Или писатель статьи говорит о вещах, которые не являются стандартными и специфичными для компилятора MSVС++?

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

struct Data
{
    char ID;
    char pad1[3];
    short Number;
    char pad2[2];
    char Name[5];
    char pad3[3];
    int Number2;
    double Value;
} DataVal;

DataVal.ID = 0;
DataVal.Number = 1000;
DataVal.Number2 = 0xFFFFFF;
DataVal.Value = 1.2;

Если это правда, заменит ли Name[5] и pad3[3] на std::string Name; какую-либо разницу в выравнивании памяти? Будут ли назначения переменных Number2 и Value еще атомарными?

Может кто-нибудь объяснит?

Ответ 1

Эта рекомендация относится к архитектуре. Это верно для x86 и x86_64 (в низкоуровневом программировании). Вы также должны убедиться, что компилятор не переупорядочивает ваш код. Для этого вы можете использовать "барьер памяти компилятора".

Низкоуровневое атомное чтение и запись для x86 описано в справочных руководствах Intel "Руководство разработчика программного обеспечения для разработчиков Intel® 64 и IA-32" Том 3A (http://www.intel.com/Assets/PDF/manual/253668.pdf), раздел 8.1.1

8.1.1 Гарантированные атомные операции

Процессор Intel486 (и более новые процессоры с тех пор) гарантирует, что следующее операции основной памяти всегда будут выполняться атомарно:

  • Чтение или запись байта
  • Чтение или запись слова, выровненного на 16-битной границе
  • Чтение или запись двойного слова, выровненного на 32-битной границе

Процессор Pentium (и более новые процессоры с тех пор) гарантирует, что следующее дополнительные операции памяти всегда выполняются атомарно:

  • Чтение или запись квадлового слова, выровненного на 64-битной границе
  • 16-разрядный доступ к нераскрытым ячейкам памяти, которые подходят к 32-разрядной шине данных

Процессоры семейства P6 (и более новые процессоры с тех пор) гарантируют, что следующие операция дополнительной памяти всегда будет выполняться атомарно:

  • Unaligned 16-, 32- и 64-разрядные обращения к кэшированной памяти, которые вписываются в кеш линия

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

Другие руководства Intel рекомендуют эту техническую документацию:

http://software.intel.com/en-us/articles/developing-multithreaded-applications-a-platform-consistent-approach/

Ответ 2

Я думаю, что вы неверно истолковываете цитату.

Атомность может быть гарантирована в заданной архитектуре с использованием конкретных инструкций (соответствующих этой архитектуре). В статье MSDN объясняется, что чтение и запись на встроенных типах С++ может быть атомарной в архитектуре x86.

Однако стандарт С++ не предполагает, что такое архитектура, поэтому Standard не может делать такие гарантии. Действительно, С++ используется во встроенном программном обеспечении, где аппаратная поддержка намного ограничена.

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

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

Ответ 3

Стандарт С++ не гарантирует атомного поведения. На практике, однако, простые операции загрузки и хранения будут атомарными, как говорится в статье.

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

*counter = 0; // this is atomic on most platforms
*counter++;   // this is NOT atomic on most platforms

Ответ 4

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

Нижняя строка для разработчика приложения - это либо примитивы, что гарантии ОС будут атомарными или использовать соответствующие блокировки.

Ответ 5

IMO, статья содержит некоторые предположения о базовой архитектуре. Поскольку С++ имеет только некоторые минималистические требования к архитектуре, никакие гарантии, например, об атомарности, не могут быть даны в стандарте. Например, байт должен быть не менее 8 бит, но у вас может быть архитектура, где байт равен 9 бит, но теоретически int 16...

Поэтому, когда компилятор специфичен для архитектуры x86, можно использовать определенные функции.

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

Ответ 6

Я думаю, что то, что они пытаются получить, - это то, что типы данных, реализованные из-за аппаратного обеспечения, обновляются в аппаратном обеспечении таким образом, что чтение из другого потока никогда не даст вам "частично" обновленное значение.

Рассмотрим 32-битное целое число на 32-битной машине. Он записывается или читается полностью в 1 цикл инструкций, тогда как типы данных больших размеров, например 64-битные int на 32-битной машине, потребуют больше циклов, поэтому теоретически нить, записывающая их, может прерываться между этими циклами ergo, это значение не в правильном состоянии.

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

Причина, по которой это не в стандарте, заключается в том, что, как указано в статье, речь идет о том, как современные процессоры выполняют инструкции. Ваш стандартный код на C/С++ должен работать точно так же на 16 или 64-битной машине (только с разницей в производительности), однако, если вы предполагаете, что вы будете выполнять только на 64-битной машине, то все 64 бита или меньше атома. (SSE и т.д.).

Ответ 7

Я думаю, что atomicity, как упоминается в статье, мало практического использования. Это означает, что вы будете читать/писать допустимое значение, но, возможно, устарели. Поэтому, читая int, вы полностью его прочитаете, а не 2 байта от старого значения и других 2 байтов от нового значения, которое в настоящее время записывается другим потоком.

Для общей памяти важны барьеры памяти. И они гарантируются примитивами синхронизации, такими как типы С++ 0x atomic, mutexes и т.д.

Ответ 8

Я не думаю, что изменение char Name[5] до std::string Name будет иметь значение , если вы используете его только для индивидуальных назначений символов, так как оператор индекса вернет прямую ссылку на базовый символ, Полное назначение строк не является атомарным (и вы не можете сделать это с помощью массива char, поэтому я предполагаю, что вы так и не подумали использовать его таким образом).