5.1.2. Объекты, ячейки памяти и параллелизм
Для многопоточных приложений на С++ понятие ячейки памяти критически важно. Если два потока обращаются к разным ячейкам памяти, то никаких проблем не возникает и всё работает, как надо. Но если потоки обращаются к одной и той же ячейке, то необходима осторожность. Если ни один поток не обновляет ячейку памяти, то всё хорошо — доступ к данным для чтения не нуждается ни в защите, ни в синхронизации. Если же какой-то поток модифицирует данные, то возможно состояние гонки, описанное в главе 3.
Чтобы избежать гонки, необходимо принудительно упорядочить обращения из двух потоков. Один из возможных способов такого упорядочения дают мьютексы (см. главу 3) — если захватывать один и тот же мьютекс перед каждым обращением, то одновременно получить доступ к ячейке памяти сможет только один поток, так что упорядочение налицо. Другой способ упорядочить доступ из двух потоков — воспользоваться свойствами синхронизации, присущими атомарным операциям (о том, что это такое, см. раздел 5.2) над теми же или другими ячейками памяти. Такое использование атомарных операций описано в разделе 5.3. Если к одной и той же ячейке обращаются более двух потоков, то упорядочение должно быть определено для каждой пары.
Если два обращения к одной и той же ячейке памяти из разных потоков не упорядочены и одно или оба обращения не являются атомарными и одно или оба обращения являются операциями записи, то имеет место гонка за данными, что приводит к неопределенному поведению.
Эта фраза критически важна: неопределенное поведение — один из самых грязных закоулков С++. Согласно стандарту языка, любое неопределенное поведение отменяет всякие гарантии — поведение всего приложения становится неопределённым, и оно может делать все, что угодно. Я знаю один пример неопределённого поведения, в результате которого загорелся монитор. Хотя маловероятно, что такое приключится с вами, гонка за данными безусловно является серьезной ошибкой, которой следует всеми силами избегать.
В этой фразе есть и еще один важный момент: избежать неопределенного поведения поможет использование атомарных операций для доступа к ячейке памяти, за которую возможна гонка. Саму гонку это не предотвращает — какая именно атомарная операция первой получит доступ к ячейке памяти, все равно не определено, — но программа тем не менее возвращается в область определённого поведения.
Прежде чем мы перейдем к атомарным операциям, нужно разобраться еще в одной важной концепции, касающейся объектов и ячеек памяти: порядке модификации.