атомарное чтение и запись gcc

У меня есть многопоточное приложение, в котором я один поток-производитель (основной) и несколько потребителей.

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

Насколько я понимаю, я могу использовать встроенные атомарные функции из gcc: https://gcc.gnu.org/onlinedocs/gcc-4.1.2/gcc/Atomic-Builtins.html

однако в нем нет функции просто чтения переменной, над которой я хочу работать.

Так что в основном мой вопрос.

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

or

  1. мне нужна какая-то другая функция для чтения из переменной. и что это за функция

person John Smith    schedule 10.06.2014    source источник
comment
Не могли бы вы использовать __sync_fetch_and_add(ptr, 0) для чтения ptr?   -  person i_am_jorf    schedule 10.06.2014
comment
Вы можете безопасно читать 32-битную целочисленную переменную на x86 без использования атомики. Возможно, вам потребуется объявить переменную как volatile, чтобы компилятор не оптимизировал чтение.   -  person markgz    schedule 10.06.2014
comment
@markgz Это так неправильно.   -  person nwp    schedule 10.06.2014
comment
Атомарность - это перегруженный термин. Одно дело - считывать значения за один раз, гарантируя, что они не разделены, например, на строку кэша или страницу, и могут извлекать частичные байты данных за разное время наблюдения. Другое дело - убедиться, что эти операции чтения синхронизируются с другими операциями чтения / записи с использованием некоторой модели упорядочения памяти.   -  person Leeor    schedule 10.06.2014
comment
@markgz Чтобы уточнить комментарий nwp ... Если бы вы сказали, что можете безопасно читать 8-битную целочисленную переменную без использования атомики, в это было бы намного легче поверить (хотя я все еще не уверен на 100% в этом в совершенно общем случае). Но 32-битные значения могут быть легко разделены между строкой кэша или границами страницы или просто смещены, как намекает Лиор, что делает эту безопасность совершенно неверной для любой переменной, содержащей более одного байта.   -  person twalberg    schedule 10.06.2014
comment
@twalberg Дело не в этом. x86 гарантирует атомарные чтения и записи целых чисел, если они не расположены в разных строках кеша. Но C и компиляторы этого не делают. Они предполагают, что гонка данных не произойдет, и оптимизируют, основываясь на этом предположении, делая оптимизацию неверной. То же самое и с целочисленными переполнениями со знаком. Насколько я знаю, каждая существующая платформа будет просто заморачиваться. C говорит, что это неопределенное поведение, которое нарушит код. См. этот документ, чтобы узнать, почему гонка данных нарушает код, даже если два потока пишут такое же значение.   -  person nwp    schedule 11.06.2014
comment
В руководстве разработчика программного обеспечения Intel, том 3, раздел 8.1.1 на стр. 8-2, говорится, что процессоры 486 и новее гарантируют, что чтение [...] 32-битного [...] целого числа [...] с естественным выравниванием будет атомарным. Вдобавок P6 и более новые процессоры гарантируют атомарность для 32-битного целого числа, выровненного с ошибкой, которое не пересекает границу строки кэша.   -  person markgz    schedule 11.06.2014
comment
@markgz Вот что я написал. Код все равно не будет работать даже на этих платформах, если компилятор оптимизирует код.   -  person nwp    schedule 11.06.2014


Ответы (4)


Определите «безопасно».

Если вы просто используете обычное чтение на x86 для естественно выровненных 32-битных или меньших данных, чтение будет атомарным, поэтому вы всегда будете читать действительное значение, а не то, которое содержит некоторые байты, записанные одним потоком, а некоторые - другим. Если что-то из этого не соответствует действительности (не x86, не выровнено естественным образом, больше 32 бит ...), все ставки отключены.

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

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

__sync_val_compare_and_swap(ptr, 0, 0)

or

__sync_add_and_fetch(ptr, 0)

or

__sync_sub_and_fetch(ptr, 0)

или что-то еще

person moonshadow    schedule 10.06.2014
comment
Когерентность кэша на процессорах Intel гарантирует, что если какой-либо процессор выполнит запись в общую линию, эта строка будет признана недействительной в кэше разделяемого процессора, и обновленное значение будет прочитано в следующий раз, когда разделяемый процессор прочитает эту строку. - person markgz; 11.06.2014
comment
@markgz x86 гарантии не будут переведены на гарантии для C. - person nwp; 11.06.2014
comment
@nwp не могли бы вы объяснить, почему нет? - person markgz; 11.06.2014
comment
@markgz Пример: int i = INT_MAX; if (i+1 < i) printf("overflow"); не обязательно печатает переполнение. Условие i+1 < i никогда не может быть истинным в C, потому что переполнение - UB. x86 гарантирует, что INT_MAX +1 == INT_MIN. C не делает. C на x86 нет. Если вы попробуете то же самое с unsigned int и UINT_MAX, вы получите переполнение, потому что оно определено. То же самое с потокобезопасностью ints. x86 гарантирует это. C на x86 нет. Подробную информацию о том, что компиляторы могут делать с гонками данных, см. В этом документе. - person nwp; 11.06.2014

Если ваш компилятор поддерживает это, вы можете использовать атомарные типы C11. Они представлены в разделе 7.17 стандарта, но, к сожалению, являются необязательными, поэтому вам нужно будет проверить, определено ли __STDC_NO_ATOMICS__, чтобы, по крайней мере, выдать значимую ошибку, если она не поддерживается.

С gcc вам, по-видимому, нужна как минимум версия 4.9, потому что в противном случае заголовок отсутствует (вот такой вопрос по этому поводу, но я могу '' не проверять, потому что у меня нет GCC-4.9).

person cmaster - reinstate monica    schedule 10.06.2014

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

На странице, на которую вы ссылаетесь, перечислены атомарные операции для writer, но ничего не говорится о том, как следует читать такие переменные. Ответ заключается в том, что другие ядра вашего ЦП будут «видеть» обновленные значения, но ваш компилятор может «кэшировать» старое значение в регистре или в стеке. Чтобы предотвратить такое поведение, я предлагаю вам объявить переменную volatile, чтобы ваш компилятор не кэшировал старое значение.

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

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

person Sophit    schedule 10.06.2014
comment
volatile недостаточно: он влияет только на компилятор, что в современной многоядерной системе со слабо согласованной памятью - наименьшая из ваших проблем. В качестве побочного эффекта решения других проблем volatile становится излишним. stackoverflow.com/questions/2484980/ - person moonshadow; 10.06.2014
comment
Да, я рассказал об этом. Атомарные встроенные функции GCC плохо спроектированы. Гарантии, предоставляемые volatile, достаточны для его очень простого варианта использования в сочетании с атомарностью GCC. Для более сложных случаев использования ему потребуется правильно спроектированная система. Если у вас есть лучшая альтернатива volatile для использования со встроенными командами GCC, поделитесь. - person Sophit; 10.06.2014
comment
хм, для моего случая использования этого должно быть достаточно, но поскольку я хочу использовать это в производственном приложении, я хочу использовать «лучший» из возможных способов. Вы можете мне предложить какую-нибудь альтернативу? - person John Smith; 11.06.2014

Если бы я понял проблему, я бы не использовал атомарные переменные для счетчиков. У каждого рабочего потока может быть отдельный счетчик, который он обновляет локально, главный поток может читать весь массив счетчиков для приблизительного значения моментального снимка, так что это становится проблемой 1 потребитель 1 производитель. Память может быть сделана видимой для главного потока, например, каждые 5 секунд с помощью __sync_synchronize () или аналогичного.

person hdante    schedule 10.06.2014