Предположим, что у кого-то есть код на основе блокировки, такой как следующий, где мьютексы используются для защиты от ненадлежащего одновременного чтения и записи
mutex.get() ; // get a lock.
T localVar = pSharedMem->v ; // read something
pSharedMem->w = blah ; // write something.
pSharedMem->z++ ; // read and write something.
mutex.release() ; // release the lock.
Если предположить, что сгенерированный код был создан в программном порядке, все еще существует требование для соответствующих барьеров аппаратной памяти, таких как isync, lwsync,.acq,.rel. Я возьму на этот вопрос, что реализация мьютекса позаботится об этой части, предоставив гарантию, что pSharedMem читает и записывает все, происходит "после" get и "before" release() [но окружающие чтения и записи могут попасть в критический раздел, как я ожидаю, является нормой для реализации мьютексов]. Я также предполагаю, что волатильный доступ используется в реализации мьютекса, где это необходимо, но этот volatile НЕ используется для данных, защищенных мьютексом (понимание того, почему volatile не является требованием для защищенных мьютексом данных, действительно является частью этот вопрос).
Я хотел бы понять, что мешает компилятору перемещать доступ pSharedMem за пределы критической области. В стандартах C и С++ я вижу, что существует концепция последовательности. Большая часть текста точки последовательности в стандартах docs я нашел непонятным, но если бы я должен был догадаться, о чем речь, это утверждение о том, что код не должен переупорядочиваться через точку, где есть вызов с неизвестными побочными эффектами. Разве это его суть? Если это так, то какая свобода оптимизации имеет здесь компилятор?
С компиляторами, выполняющими сложные оптимизации, такие как межпроцессорная вставка с профилем (даже через границы файлов), даже концепция неизвестного побочного эффекта становится размытой.
Возможно, это не просто вопрос простого объяснения этого в автономном режиме, поэтому я открыт для ссылки на ссылки (предпочтительнее онлайн и нацелен на смертных программистов, а не разработчиков компиляторов и разработчиков языков).
EDIT: (в ответ на ответ Jalf)
Я упомянул инструкции по защите памяти, такие как lwsync и isync из-за проблем с переупорядочиванием процессора, о которых вы также упоминали. Я, случается, работает в той же лаборатории, что и ребята-компиляторы (по крайней мере, для одной из наших платформ), и, поговорив с разработчиками встроенных функций, я знаю, что по крайней мере для компилятора xlC __isync() и __lwsync() ( и остальные атомные свойства) также являются барьером переупорядочения кода. В нашей реализации spinlock это видно компилятору, так как эта часть нашего критического раздела встроена.
Однако предположим, что вы не использовали специальную реализацию блокировки сборки (как, например, мы, вероятно, не редкость), а просто называем общий интерфейс, такой как pthread_mutex_lock(). Там компилятор не сообщил ничего больше, чем прототип. Я никогда не видел, чтобы он предположил, что код будет нефункциональным
pthread_mutex_lock( &m ) ; pSharedMem->someNonVolatileVar++ ; pthread_mutex_unlock( &m ) ; pthread_mutex_lock( &m ) ; pSharedMem->someNonVolatileVar++ ; pthread_mutex_unlock( &m ) ;
будет нефункциональным, если переменная не будет изменена на летучую. У этого приращения будет последовательность загрузки/увеличения/сохранения в каждом из обратных блоков кода и не будет работать корректно, если значение первого приращения будет сохранено в регистре для второго.
Похоже, что неизвестные побочные эффекты pthread_mutex_lock() - это то, что защищает этот пример назад от обратного приращения от неправильного поведения.
Я говорю о том, что семантика такой последовательности кода в потоковой среде на самом деле не строго покрывается спецификациями языка C или С++.