Как гарантировать, что 64-битная запись является атомарной?
Когда можно гарантировать, что 64-битная запись будет атомарной при программировании на C на платформе на базе x86 на базе Intel (в частности, Mac на базе Intel, работающей под MacOSX 10.4 с использованием компилятора Intel)? Например:
unsigned long long int y;
y = 0xfedcba87654321ULL;
/* ... a bunch of other time-consuming stuff happens... */
y = 0x12345678abcdefULL;
Если другой поток проверяет значение y после завершения выполнения первого присваивания y, я хотел бы убедиться, что он видит либо значение 0xfedcba87654321, либо значение 0x12345678abcdef, а не некоторую смесь из них. Я хотел бы сделать это без блокировки и, если возможно, без дополнительного кода. Я надеюсь, что при использовании 64-битного компилятора (64-разрядного компилятора Intel) в операционной системе, поддерживающей 64-разрядный код (MacOSX 10.4), эти 64-битные записи будут атомарными. Это всегда так?
Ответы
Ответ 1
Лучше всего избегать попытки создать свою собственную систему из примитивов и вместо этого использовать блокировку, если она не будет действительно отображаться как горячая точка при профилировании. (Если вы считаете, что можете быть умными и избегать блокировок, не делайте этого.Вы не знаете, что общее "вы", которое включает меня и всех остальных.) Вы должны как минимум использовать блокировку спина, см. spinlock (3). И что бы вы ни делали, не стараются реализовать "свои собственные" блокировки. Вы ошибетесь.
В конечном счете вам нужно использовать любые блокирующие или атомные операции, которые предоставляет ваша операционная система. Получение таких типов точно вправо в во всех случаях чрезвычайно сложно. Часто это может включать знание таких вещей, как ошибки для конкретных версий конкретного процессора. ( "О, версия 2.0 этого процессора не выполняла привязку кэш-когерентности в нужное время, исправлена в версии 2.0.1, но на 2.0 вам нужно вставить NOP
".) Просто похлопывая a volatile
ключевое слово для переменной в C почти всегда недостаточно.
В Mac OS X это означает, что вам нужно использовать функции, перечисленные в atomic (3), чтобы выполнить поистине атомный-все-все- Процессоры работают с 32-разрядными, 64-разрядными и указательными размерами. (Используйте последний для любых атомных операций над указателями, чтобы вы автоматически совместили 32/64-разрядные.) Это делается независимо от того, хотите ли вы делать такие вещи, как атомное сравнение и обмен, инкремент/декремент, блокировка спина или стек/очередь управление. К счастью, spinlock (3), atomic (3), и барьер (3) должны работать правильно на всех процессорах, поддерживаемых Mac OS X.
Ответ 2
В x86_64 как компилятор Intel, так и gcc поддерживают некоторые собственные функции атомного управления. Здесь gcc документация о них: http://gcc.gnu.org/onlinedocs/gcc-4.1.0/gcc/Atomic-Builtins.html
Компилятор Intel также говорит о них здесь: http://softwarecommunity.intel.com/isn/downloads/softwareproducts/pdfs/347603.pdf (страница 164 или около того).
Ответ 3
В Intel MacOSX вы можете использовать встроенные системные операции. Не существует атомарного атома get или set для 32 или 64-битных целых чисел, но вы можете построить это из предоставленного CompareAndSwap. Возможно, вы захотите найти документацию XCode для различных функций OSAtomic. Я написал 64-битную версию ниже. 32-разрядная версия может быть выполнена с аналогичными именованными функциями.
#include <libkern/OSAtomic.h>
// bool OSAtomicCompareAndSwap64Barrier(int64_t oldValue, int64_t newValue, int64_t *theValue);
void AtomicSet(uint64_t *target, uint64_t new_value)
{
while (true)
{
uint64_t old_value = *target;
if (OSAtomicCompareAndSwap64Barrier(old_value, new_value, target)) return;
}
}
uint64_t AtomicGet(uint64_t *target)
{
while (true)
{
int64 value = *target;
if (OSAtomicCompareAndSwap64Barrier(value, value, target)) return value;
}
}
Обратите внимание, что функции Apple OSAtomicCompareAndSwap атомарно выполняют операцию:
if (*theValue != oldValue) return false;
*theValue = newValue;
return true;
Мы используем это в приведенном выше примере, чтобы создать метод Set, сначала захватив старое значение, а затем попытаемся поменять значение целевой памяти. Если своп преуспевает, это указывает на то, что значение памяти по-прежнему остается старым значением во время свопинга, и ему присваивается новое значение во время свопинга (которое само является атомарным), поэтому мы закончили. Если это не удастся, тогда какой-то другой поток вмешался, изменив значение между ними, когда мы его схватили, и когда мы попытались reset его. Если это произойдет, мы можем просто зациклиться и повторить попытку с минимальным штрафом.
Идея метода Get заключается в том, что мы можем сначала получить значение (которое может быть или не быть фактическим значением, если другой поток мешает). Затем мы можем попытаться поменять значение с собой, просто чтобы проверить, что начальный захват был равен атомному значению.
Я не проверял это против моего компилятора, поэтому, пожалуйста, извините за любые опечатки.
Вы упомянули OSX специально, но в случае, если вам нужно работать на других платформах, Windows имеет ряд функций Interlocked *, и вы можете искать для них документацию MSDN. Некоторые из них работают на Windows 2000 Pro и более поздних версиях, а некоторые (особенно некоторые из 64-битных функций) являются новыми с Vista. На других платформах версии GCC 4.1 и более поздние версии имеют множество функций __sync *, таких как __sync_fetch_and_add(). Для других систем вам может потребоваться использовать сборку, и вы можете найти некоторые реализации в браузере SVN для проекта HaikuOS, внутри src/system/libroot/os/arch.
Ответ 4
В соответствии с главой 7 Часть 3A - Руководство по системному программированию Intel руководства по процессорам, доступ к четырем слотам будет выполняться атомарно, если он выровнен на 64-битной границе, на Pentium или новее и не выровнен (если все еще в строке кэша) на P6 или новее. Вы должны использовать volatile
, чтобы убедиться, что компилятор не пытается кэшировать запись в переменной, и вам может потребоваться использовать процедуру блокировки памяти, чтобы убедиться, что запись происходит в правильном порядке.
Если вам нужно основывать значение, записанное на существующее значение, вы должны использовать свою операционную систему. Функции блокировки (например, Windows имеет InterlockedIncrement64).
Ответ 5
На X86 самым быстрым способом атомарной записи выровненного 64-битного значения является использование FISTP. Для невыровненных значений вам необходимо использовать CAS2 (_InterlockedExchange64). Операция CAS2 довольно медленная из-за BUSLOCK, хотя часто бывает проще проверить выравнивание и выполнить версию FISTP для выровненных адресов. В самом деле, вот как Intel Threaded Building Blocks реализует Atomic 64-разрядные записи.
Ответ 6
GCC имеет встроенные функции для атомных операций; Я подозреваю, что вы можете сделать подобное с другими компиляторами. Никогда не полагайтесь на компилятор для атомных операций; оптимизация почти наверняка приведет к риску сделать даже явно атомные операции неатомными, если вы явно не сообщите компилятору об этом.
Ответ 7
Если вы хотите сделать что-то подобное для interthread или interprocess communication, тогда вам нужно иметь больше, чем просто атомную гарантию чтения/записи. В вашем примере кажется, что вы хотите, чтобы записанные значения указывали, что выполняется некоторая работа и/или была завершена. Вам нужно будет сделать несколько вещей, не все из которых являются переносимыми, чтобы убедиться, что компилятор сделал что-то в том порядке, в котором вы их делаете (ключевое слово volatile может помочь в определенной степени), и эта память последовательна. Современные процессоры и кэши могут выполнять работу не по порядку без ведома компилятора, поэтому вам действительно нужна поддержка платформы (то есть, блокировки или связанные с платформой API), чтобы делать то, что вы хотите сделать.
"Забор памяти" или "барьер памяти" - это термины, которые вы можете исследовать.
Ответ 8
Последняя версия ISO C (C11) определяет набор атомных операций, включая atomic_store(_explicit)
. См. эта страница для получения дополнительной информации.
Вторая наиболее переносимая реализация атомистики - это внутренняя среда GCC, о которой уже упоминалось. Я нахожу, что они полностью поддерживаются компиляторами GCC, Clang, Intel и IBM, и - в последний раз, когда я проверял - частично поддерживался компиляторами Cray.
Одним из явных преимуществ атомарности C11 - в дополнение ко всему стандарту ISO - является то, что они поддерживают более точный рецепт согласования памяти. Насколько я знаю, атомизация GCC подразумевает полный барьер памяти.