EDIT:
Бен прав (и я идиот, говоря, что это не так), что есть вероятность, что процессор переупорядочит инструкции и выполнит их одновременно по нескольким конвейерам. Это означает, что значение = 1 может быть установлено до того, как конвейер выполнит «работу». В свою защиту (не полный идиот?) Я никогда не видел, чтобы это произошло в реальной жизни, и у нас есть обширная библиотека потоков, и мы проводим исчерпывающие долгосрочные тесты, и этот шаблон используется повсеместно. Я бы видел это, если бы это происходило, но ни один из наших тестов никогда не дал сбой или дал бы неправильный ответ. Но ... Бен прав, возможность существует. Это, вероятно, происходит все время в нашем коде, но переупорядочение не устанавливает флаги достаточно рано, чтобы потребители данных, защищенных флагами, могли использовать данные до его завершения. Я буду изменять наш код, чтобы включить барьеры, потому что нет никакой гарантии, что это продолжит работать в дикой природе. Я считаю, что правильное решение похоже на это:
Темы, которые читают значение:
...
if (value)
{
__sync_synchronize(); // don't pipeline any of the work until after checking value
DoSomething();
}
...
Поток, который устанавливает значение:
...
DoStuff()
__sync_synchronize(); // Don't pipeline "setting value" until after finishing stuff
value = 1; // Stuff Done
...
При этом я обнаружил, что это является простым объяснением барьеров.
БАРЬЕР КОМПИЛЕРА
Барьеры памяти влияют на процессор. Барьеры компилятора влияют на компилятор. Volatile не будет удерживать компилятор от переупорядочения кода. Здесь для получения дополнительной информации.
Я полагаю, что вы можете использовать этот код, чтобы gcc не переставлял код во время компиляции:
#define COMPILER_BARRIER() __asm__ __volatile__ ("" ::: "memory")
Так, может быть, именно это и следует сделать?
#define GENERAL_BARRIER() do { COMPILER_BARRIER(); __sync_synchronize(); } while(0)
Темы, которые читают значение:
...
if (value)
{
GENERAL_BARRIER(); // don't pipeline any of the work until after checking value
DoSomething();
}
...
Поток, который устанавливает значение:
...
DoStuff()
GENERAL_BARRIER(); // Don't pipeline "setting value" until after finishing stuff
value = 1; // Stuff Done
...
Использование GENERAL_BARRIER () не позволяет gcc переупорядочить код, а также не позволяет процессору переупорядочивать код. Теперь мне интересно, не будет ли gcc переупорядочивать код поверх встроенного барьера памяти, __sync_synchronize (), что сделает использование COMPILER_BARRIER избыточным.
X86
Как указывает Бен, разные архитектуры имеют разные правила относительно того, как они переставляют код в конвейерах выполнения. Intel кажется довольно консервативным. Таким образом, барьеры могут не потребоваться почти столько же от Intel. Тем не менее, это не очень хорошая причина избегать барьеров, поскольку это может измениться.
ОРИГИНАЛЬНЫЙ ПОЧТА:
Мы делаем это все время. это совершенно безопасно (не для всех ситуаций, но много). Наше приложение работает на 1000 серверов в огромной ферме с 16 экземплярами на сервер, и у нас нет условий гонки. Вы правильно задаетесь вопросом, почему люди используют мьютексы для защиты уже атомарных операций. Во многих ситуациях замок - пустая трата времени. Чтение и запись в 32-битные целые числа на большинстве архитектур является атомарным. Не пытайтесь делать это с 32-битными битовыми полями!
Переупорядочение записи процессора не повлияет на один поток, считывающий глобальное значение, установленное другим потоком. Фактически, результат использования блокировок такой же, как и результат не без блокировок. Если вы выигрываете гонку и проверяете значение до того, как оно изменилось ... ну, это то же самое, что выиграть гонку, чтобы заблокировать значение, чтобы никто другой не мог изменить его, пока вы его читаете. Функционально то же самое.
Ключевое слово volatile говорит компилятору не сохранять значение в регистре, а продолжать ссылаться на исходную ячейку памяти. это не должно иметь никакого эффекта, если вы не оптимизируете код. Мы обнаружили, что компилятор довольно умен в этом и еще не сталкивался с ситуацией, когда volatile что-то изменило. Компилятор, кажется, довольно хорош в поиске кандидатов для оптимизации регистров. Я подозреваю, что ключевое слово const может стимулировать оптимизацию регистра для переменной.
Компилятор может изменить порядок кода в функции, если он знает, что конечный результат не будет отличаться.Я не видел, чтобы компилятор делал это с глобальными переменными, потому что компилятор понятия не имеет, как изменение порядка глобальной переменной повлияет на код вне непосредственной функции.
Если функция работает, вы можете контролировать уровень оптимизации на уровне функции, используя __attrribute __.
Теперь, если вы используете этот флаг в качестве шлюза, чтобы разрешить только один потокгруппы, выполняющей какую-либо работу, , которая не будет работать .Пример: поток A и поток B оба могут прочитать флаг.Поток A запланирован.Поток B устанавливает флаг в 1 и начинает работать.Поток A просыпается, устанавливает флаг в 1 и начинает работать.По электронной почте Ой!Чтобы избежать блокировок и все-таки делать что-то подобное, вам нужно изучить атомарные операции, в частности gcc atomic builtins , например __sync_bool_compare_and_swap (значение, старое, новое).Это позволяет вам установить значение = новое, если значение в настоящее время старое.В предыдущем примере, если значение = 1, только один поток (A или B) мог выполнить __sync_bool_compare_and_swap (& value, 1, 2) и изменить значение с 1 на 2. Потерянный поток потерпит неудачу.__sync_bool_compare_and_swap возвращает успешное завершение операции.
В глубине есть «блокировка» при использовании атомарных встроенных функций, но это аппаратная инструкция и очень быстрая по сравнению с использованием мьютексов.
Тем не менее, используйте мьютексы, когда вам нужно изменить много значений одновременно.атомарные операции (на сегодняшний день) работают только тогда, когда все данные, которые должны изменяться атомарно, могут помещаться в непрерывные 8, 16, 32, 64 или 128 бит.