К сожалению, изменчивая семантика довольно слабая. Концепция volatile на самом деле не предназначена для использования в потоках.
Potatoswatter прав, что вызов примитивов синхронизации ОС обычно не позволяет оптимизирующему компилятору поднять чтение num из цикла. Но это работает по той же причине, по которой метод доступа работает ... случайно.
Компилятор видит, что вы вызываете функцию, которая не сразу доступна для встраивания или анализа, поэтому он должен предположить, что любая переменная, которая может использоваться другой функцией, может быть прочитана или изменена в этой непрозрачной функции. Поэтому перед выполнением вызова компилятору необходимо записать все эти «глобальные» переменные обратно в память.
В corensic мы добавили встроенную функцию в jinx.h, которая делает это более прямым способом. Примерно так:
inline void memory_barrier() { asm volatile("nop" ::: "memory"); }
Это довольно тонко, но эффективно говорит компилятору (gcc), что он не может избавиться от этого куска непрозрачного asm и что opaque asm может читать или записывать любой глобально видимый фрагмент памяти. Это эффективно останавливает компилятор от переупорядочения нагрузок / хранилищ через эту границу.
Для вашего примера:
memory_barrier ();
while (num == 0) {
memory_barrier ();
...
}
Теперь чтение num застряло на месте. И, что еще важнее, он застрял на месте по отношению к другому коду. Таким образом, вы могли бы иметь:
while (flag == 0) { memory_barrier(); } // spin
process data[0..N]
И другой поток делает:
populate data[0..N]
memory_barrier();
flag = 1;
PS. Если вы делаете такие вещи (по сути, создавая свои собственные примитивы синхронизации), выигрыш может быть большим, но риск для качества велик. Jinx особенно хорош в обнаружении ошибок в этих структурах без блокировки. Поэтому вы можете использовать его или какой-либо другой инструмент для проверки этого материала.
PPS. У сообщества linux есть хороший пост об этом, который называется «volatile считается вредным», зацените его.