Я изучал повторный вход в программирование. На этом сайте IBM (действительно хороший). Я основал код, скопированный ниже. Это первый код, который катится по сайту.
Код пытается показать проблемы, связанные с общим доступом к переменной при нелинейной разработке текстовой программы (асинхронность), печатая два значения, которые постоянно меняются в «опасном контексте».
#include <signal.h>
#include <stdio.h>
struct two_int { int a, b; } data;
void signal_handler(int signum){
printf ("%d, %d\n", data.a, data.b);
alarm (1);
}
int main (void){
static struct two_int zeros = { 0, 0 }, ones = { 1, 1 };
signal (SIGALRM, signal_handler);
data = zeros;
alarm (1);
while (1){
data = zeros;
data = ones;
}
}
Проблемы возникли, когда я попытался запустить код (или, лучше сказать, не появился). Я использовал gcc версии 6.3.0 20170516 (Debian 6.3.0-18 + deb9u1) в конфигурации по умолчанию. Неправильный вывод не происходит. Частота получения «неправильных» парных значений равна 0!
Что происходит в конце концов? Почему нет проблем при повторном входе с использованием статических глобальных переменных?
Ответы:
Это не совсем вход ; Вы не запускаете функцию дважды в одном и том же потоке (или в разных потоках). Вы можете получить это с помощью рекурсии или передачи адреса текущей функции в виде функции-указателя функции обратного вызова в другую функцию. (И это не было бы небезопасно, потому что это было бы синхронно).
Это просто обычная гонка данных UB (Undefined Behavior) между обработчиком сигнала и основным потоком: только
sig_atomic_t
гарантировано безопасное для этого . Другие могут работать, как в вашем случае, когда 8-байтовый объект может быть загружен или сохранен с одной инструкцией на x86-64, и компилятор выбирает этот asm. (Как показывает ответ @ icarus).См. Программирование MCU - оптимизация C ++ O2 прерывается во время цикла - обработчик прерываний в одноядерном микроконтроллере - это то же самое, что и обработчик сигналов в однопоточной программе. В этом случае результат UB - то, что груз был поднят из петли.
Ваш тестовый случай разрыва действительно происходит из-за гонок данных UB, вероятно, был разработан / протестирован в 32-битном режиме или с более старым тупым компилятором, который загружал члены структуры отдельно.
В вашем случае компилятор может оптимизировать хранилища из бесконечного цикла, потому что ни одна свободная от UB программа никогда не сможет их наблюдать.
data
не является_Atomic
илиvolatile
, и нет никаких других побочных эффектов в цикле. Так что никакой читатель не сможет синхронизироваться с этим писателем. Фактически это происходит, если вы компилируете с включенной оптимизацией ( Godbolt показывает пустой цикл внизу main). Я также изменил структуру на дваlong long
, и gcc использует одноmovdqa
16-байтовое хранилище перед циклом. (Это не является гарантированным атомарным, но на практике это происходит практически на всех процессорах, при условии, что он выровнен, или на Intel просто не пересекает границу строки кэша. Почему целочисленное присваивание для естественно выровненной переменной атомарно на x86? )Таким образом, компиляция с включенной оптимизацией также нарушит ваш тест и будет показывать вам одно и то же значение каждый раз. C не является переносимым языком ассемблера.
volatile struct two_int
также заставит компилятор не оптимизировать их, но не заставит его загружать / хранить всю структуру атомарно. (Однако это также не помешало бы сделать это.) Обратите внимание, чтоvolatile
это не предотвращает гонку данных UB, но на практике этого достаточно для связи между потоками, и это было то, как люди создавали атомарную структуру вручную (наряду с встроенным ассемблером). до C11 / C ++ 11, для нормальной архитектуры ЦП. Они кэш-когерентный такvolatile
это на практике в основном аналогична_Atomic
сmemory_order_relaxed
для чистой нагрузки и чистого-магазина, если они используются для типов достаточно узко , что компилятор будет использовать одну команду , так что вы не получите слезотечение. И конечноvolatile
не имеет никаких гарантий от стандарта ISO C против написания кода, который компилируется с использованием asm_Atomic
и mo_relaxed.Если у вас есть функция , которая сделала
global_var++;
по принципуint
илиlong long
запускать от основной и асинхронно из обработчика сигнала, который был бы способ использовать повторно entrancy для создания данных гонки UB.В зависимости от того, как он скомпилирован (в место назначения памяти inc или add, или для разделения загрузки / inc / store), он будет атомарным или нет относительно обработчиков сигналов в том же потоке. См. Может ли num ++ быть атомарным для int num? подробнее об атомарности на x86 и в C ++. ( Атрибут C11
stdatomic.h
и_Atomic
обеспечивает эквивалентную функциональность дляstd::atomic<T>
шаблона C ++ 11 )Прерывание или другое исключение не может произойти в середине инструкции, поэтому добавление к месту назначения памяти является атомарным. контекст переключается на одноядерный процессор. Только (совместимый с кэшем) DMA-писатель может «увеличить» шаг
add [mem], 1
безlock
префикса на одноядерном процессоре. Нет никаких других ядер, на которых мог бы работать другой поток.Так что это похоже на случай сигналов: обработчик сигнала запускается вместо обычного выполнения потока, обрабатывающего сигнал, поэтому он не может быть обработан в середине одной инструкции.
источник
Глядя на проводник компилятора godbolt (после добавления отсутствующего
#include <unistd.h>
), можно увидеть, что почти для любого компилятора x86_64 сгенерированный код использует QWORD-перемещения для загрузкиones
иzeros
в одной инструкции.На сайте IBM говорится,
On most machines, it takes several instructions to store a new value in data, and the value is stored one word at a time.
что это могло быть правдой для типичного процессора в 2005 году, но, как показывает код, сейчас это не так. Изменение структуры, чтобы иметь два long, а не два целых, показало бы проблему.Я ранее писал, что это было «атомное», что было ленивым. Программа работает только на одном процессоре. Каждая инструкция завершится с точки зрения этого процессора (при условии, что ничто иное не изменяет память, такую как dma).
Таким образом, на
C
уровне не определено, что компилятор выберет одну инструкцию для написания структуры, и поэтому может произойти искажение, упомянутое в статье IBM. Современные компиляторы, ориентированные на текущий процессор, используют одну инструкцию. Одной инструкции достаточно, чтобы избежать повреждения однопоточной программы.источник
int
наlong long
и скомпилировать на 32 бита. Урок в том, что вы никогда не знаете, если / когда это сломается.long long
по-прежнему компилируется в одну инструкцию для x86-64: 16 байтmovdqa
. Если вы не отключите оптимизацию, как в вашей ссылке Godbolt. (По умолчанию в GCC используется-O0
режим отладки, который полон шума хранения / перезагрузки и на него обычно не интересно смотреть.)