Задавая этот вопрос как псевдокод, а также ориентируясь как на ржавчину, так и на С++, поскольку концепции модели памяти так же
SomeFunc(){
x = counter.load(Ordering::Relaxed) //#1
counter.store(x+1, Ordering::Relaxed) //#2
y = counter.load(Ordering::Relaxed) //#3
}
Вопрос: Представьте, что SomeFunc выполняется потоком, и между #2 и #3 поток прерывается, и теперь #3 выполняется на другом ядре, в этом случае синхронизируется ли переменная счетчика с последним обновленным значением (ядро 1) при запуске на другом core2 (явного освобождения/приобретения нет). Я полагаю, вся кеш-линия + локальное хранилище потока откладывается и загружается, когда поток ненадолго переходит в спящий режим и снова работает на другом ядре?
Все это происходит в одном потоке, поэтому проблем с синхронизацией нет; он просто будет делать то, что, очевидно, делает. Даже если counter
просто старая добрая int
, а не атомарная. ЦП заботится об управлении контекстом для каждого потока. Только когда переменная используется более чем одним потоком, вам нужно беспокоиться о синхронизации.
В первую очередь следует отметить, что атомарные инструкции добавляют синхронизацию, а не убирают ее.
Вы ожидаете:
unsigned func(unsigned* counter) {
auto x = *counter;
*counter = x + 1;
auto y = *counter;
return y;
}
Чтобы вернуть что-либо, кроме исходного значения *counter
+ 1?
Но точно так же поток может перемещаться между ядрами между двумя операторами!
Приведенный выше код выполняется нормально, даже когда ядро перемещается, потому что во время переключения ОС заботится о надлежащей синхронизации между ядрами, чтобы сохранить порядок программ в пользовательском пространстве.
Итак, что происходит при использовании атомарных вычислений в одном потоке?
Ну, вы добавляете немного накладных расходов на обработку — больше синхронизации — и ОС по-прежнему заботится о правильной синхронизации во время переключения.
Следовательно, эффект будет точно таким же.
надлежащим образом управлять кэшем. Проблема не в кеше, а в буфере частного хранилища внутри каждого ядра. Кэш согласован между ядрами, на которых работает одна ОС, и может планировать потоки для них. Что на самом деле нужно ядру, так это «заботиться о синхронизации», возможно, с синхронизацией получения / освобождения, которая в любом случае нужна для его собственных данных ядра. (Или что-то более сильное для таких машин, как x86, которые имеют специальные слабо упорядоченные инструкции, такие как movntps
, которые не упорядочены атомарным выпуском/приобретением.)
Поэтому, чтобы избежать распространения распространенного заблуждения о том, что переупорядочивание памяти происходит из-за кешей, я бы рекомендовал «ОС позаботится ... о правильной синхронизации». Лучшей ментальной моделью является локальное переупорядочивание доступа к когерентному общему кешу, как в preshing.com/20120710/… . Вот почему переупорядочение IRIW происходит так редко; для этого требуется микроархитектура, которая может сделать хранилища видимыми между (логическими) ядрами, прежде чем они будут переданы в кэш. Я отредактирую, вы, конечно, можете отредактировать все, что хотите сказать.
@PeterCordes: Это хороший момент, я специально думал о буфере хранилища во время записи и использовал кеш как общий термин для чтения/записи, не думая, что в контексте процессоров кеш будет пониматься как относящийся к L1/L2/ Л3. Спасибо за редактирование!
Упреждение, переключение контекста и другие подобные механизмы ЦП прозрачны для C++, если вы соблюдаете требования синхронизации. В контексте однопоточного вызова функции нет требований к синхронизации, и переключение ядер не оказывает заметного эффекта.