Присваивает ли процессор значение памяти атомарно?

Быстрый вопрос, который меня интересовал в течение некоторого времени; Присваивает ли ЦП значения атомарно или побитно (скажем, 32-битное целое число).
Если побитно, может ли другой поток, обращающийся к этому точному местоположению, получить «часть» будущего? присвоенное значение?

Подумайте об этом:
У меня есть два потока и одна общая переменная "unsigned int" (назовите ее "g_uiVal").
Оба потока зацикливаются.
On печатает "g_uiVal" с помощью printf("%u\ n", g_uiVal).
Во втором случае просто увеличьте это число.
Будет ли поток печати когда-либо печатать что-то, что полностью не соответствует значению "g_uiVal" или является его частью?

В коде:

unsigned int g_uiVal;

void thread_writer()
{
 g_uiVal++;
}
void thread_reader()
{
 while(1)
  printf("%u\n", g_uiVal);
}

person Poni    schedule 23.05.2010    source источник


Ответы (7)


Зависит от ширины шины процессора и памяти. В контексте ПК, с чем угодно, кроме действительно древнего ЦП, доступ до 32-битных обращений является атомарным; 64-битный доступ может быть, а может и не быть. Во встроенном пространстве многие (большинство?) ЦП имеют ширину 32 бита, и нет возможности для чего-то большего, поэтому ваш int64_t гарантированно будет неатомарным.

person crazyscot    schedule 23.05.2010
comment
Можно ли в наши дни получить доступ к 32-битным значениям, пересекающим строки кэша? - person Lasse V. Karlsen; 23.05.2010
comment
@Lasse: Многие современные процессоры для настольных ПК допускают невыровненное чтение и запись, но со значительным снижением производительности. Старые или меньшие процессоры (например, для встроенных устройств), как правило, этого не делают. Некоторое время это было разделено между процессорами CISC (имеющими тенденцию поддерживать невыровненное чтение и запись) и процессорами RISC (нет), но здесь различия стираются. - person leander; 23.05.2010
comment
Широко распространены 8-, 16- и 32-битные микроконтроллеры. На AVR (с 8-битной загрузкой и сохранением) можно использовать другую ISR (процедуру обслуживания прерываний) или другой поток (если вы используете вытесняемую многозадачную операционную систему), записывающий часть переменной (или просто считывающий часть изменение, сделанное предыдущим потоком). - person nategoose; 24.05.2010
comment
Большинство встроенных процессоров являются 8- или 16-разрядными, и до сих пор используется много 4-разрядных процессоров. При этом 32-битная операция не является атомарной. С другой стороны, есть и 64-битные процессоры. В настоящее время я работаю с DSP серии TI 64x, который считается 32-битным процессором, но он может обращаться к внутренней памяти данных через 64-битную шину данных (фактически 2 x 64-битные шины) и 64-битную (и, возможно, даже 128-битную). бит) операции атомарны. - person PauliL; 24.05.2010

Я считаю, что единственный правильный ответ - "это зависит". О чем вы можете спросить?

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

Многие компиляторы предлагают «внутренние функции» для выполнения правильных атомарных операций. Это расширения, которые действуют как функции, но выдают правильный код для вашей целевой архитектуры, чтобы получить необходимые атомарные операции. Например: http://gcc.gnu.org/onlinedocs/gcc/Atomic-Builtins.html

person Evan Teran    schedule 23.05.2010

Вы сказали «по крупицам» в своем вопросе. Я не думаю, что какая-либо архитектура выполняет операции понемногу, за исключением некоторых специализированных шин последовательного протокола. Стандартные операции чтения/записи памяти выполняются с точностью до 8, 16, 32 или 64 бит. Таким образом, ВОЗМОЖНО, операция в вашем примере является атомарной.

Однако ответ сильно зависит от платформы.

  • Это зависит от возможностей процессора. Может ли аппаратное обеспечение выполнять атомарную 32-битную операцию? Вот подсказка: если переменная, над которой вы работаете, больше, чем собственный размер регистра (например, 64-битный int в 32-битной системе), она определенно НЕ атомарная.
  • Это зависит от того, как компилятор генерирует машинный код. Это могло бы превратить ваш 32-битный доступ к переменным в 4x 8-битное чтение памяти.
  • Это становится сложным, если адрес того, к чему вы обращаетесь, не выровнен по естественной границе слова машины. Вы можете столкнуться с ошибкой кеша или ошибкой страницы.

ОЧЕНЬ ВОЗМОЖНО, что вы увидите поврежденное или неожиданное значение, используя пример кода, который вы опубликовали.

Ваша платформа, вероятно, предоставляет какой-то метод выполнения атомарных операций. В случае платформы Windows — через Interlocked функции. В случае с Linux/Unix обратите внимание на тип atomic_t.

person myron-semack    schedule 24.05.2010

Чтобы добавить к тому, что было сказано до сих пор, еще одной потенциальной проблемой является кэширование. Процессоры, как правило, работают с локальным (на кристалле) кешем памяти, который может или не может быть немедленно сброшен обратно в основную память. Если блок имеет более одного ЦП, возможно, что другой ЦП не увидит изменений в течение некоторого времени после того, как их внес модифицирующий ЦП, если только не будет какой-либо команды синхронизации, информирующей все ЦП о том, что они должны синхронизировать свои кэши на кристалле. Как вы понимаете, такая синхронизация может значительно замедлить обработку.

person mfeingold    schedule 23.05.2010
comment
Но в этом случае, поскольку между потребителем и производителем нет синхронизации, это не меняет поведение. Конечно, потребитель мог прочитать старое значение, но было бы невозможно сказать, было ли это связано с несинхронизированными кэшами на кристалле или просто с планированием. Я имею в виду, что потребитель никогда не прочитает частично записанное значение из-за несинхронизированных кешей. - person Isak Savo; 23.05.2010
comment
Все зависит от того, что ожидается. Если намерение состоит в том, чтобы создавать уникальные значения - ну, эта проблема может привести к дублированию - person mfeingold; 24.05.2010

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

person Puppy    schedule 23.05.2010
comment
даже если это глобальная переменная, которая явно используется в других функциях? Сомневаюсь, что какой-либо компилятор будет так груб :) - person Isak Savo; 23.05.2010
comment
@Isak Savo: статическая (не внешняя) энергонезависимая глобальная переменная? конечно, почему бы и нет? Пометьте все переменные, используемые для управления параллелизмом, как volatile, это предотвратит оптимизацию компилятора, связанную с этими переменными. - person liori; 23.05.2010
comment
@liori: Но даже в однопоточном приложении это вполне допустимый код. Помимо плохой архитектуры, нет ничего плохого в том, что функция изменяет глобальные переменные без использования самого результата. - person Isak Savo; 23.05.2010
comment
@Isak Savo: я не говорю, что это неверный код. Я только говорю, что пока код ведет себя так, как указано, компилятор может делать что угодно... например, удалять бесполезные глобальные переменные. - person liori; 24.05.2010
comment
@liori: Полностью согласен. Но в данном случае явно используется переменная. Он отправляется в качестве аргумента функции printf, и, насколько известно компилятору, функция printf() может предотвратить конец света. - person Isak Savo; 24.05.2010
comment
@Isak Savo: компилятор знает, что поток, в котором вызывается функция thread_reader(), не изменяет эту переменную, и эта переменная не является изменчивой. Поэтому он может предположить, что его значение не изменится в цикле, и загрузить значение переменной в регистр ЦП один раз перед циклом. Насколько я знаю, gcc сделает это с -O3. - person liori; 24.05.2010
comment
(Я не могу воспроизвести это сейчас... но я уверен, что gcc делал это раньше) - person liori; 24.05.2010
comment
@liori: Значит, этот код будет взломан GCC? int myVar = 0; void f1() { myVar = 3; } void f2() { printf ("%d", myVar); } int main() { f1(); f2(); return 0;}. Я ожидаю, что этот код напечатает 3 на экране. - person Isak Savo; 24.05.2010
comment
@Isak Savo: пока все находится в одном потоке, все будет работать так, как вы ожидаете. - person liori; 24.05.2010
comment
Исак, если вы никогда не вызовете его в промежуточный период, компилятор вполне может просто поместить значение в стек и оставить его там. Во время вашего цикла другая функция никогда не вызывается, и, таким образом, компилятор вполне может ее оптимизировать. Он не может знать вашу модель потоковой передачи. Пока ваш код не прерывается в одном потоке, компилятор может это сделать. Это не работа компилятора, позволяющая работать многопоточности. - person Puppy; 24.05.2010
comment
@DeadMG: А, теперь я понимаю, что вы с Лиори имеете в виду. Я застрял в голове в более общем случае, но, снова взглянув на код OP, я понимаю, как компилятор может его оптимизировать, поскольку он не изменится во время цикла (насколько компилятору все равно). Спасибо за объяснение - person Isak Savo; 24.05.2010

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

person Chris Dodd    schedule 24.05.2010

Учитывая современные микропроцессоры (и игнорируя микроконтроллеры), 32-битное назначение является атомарным, а не побитовым.

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

person Chris O    schedule 24.05.2010