Как функции блокировки и разблокировки мьютексов предотвращают переупорядочение процессора?

Насколько мне известно, вызов функции действует как барьер компилятора, но не как барьер ЦП.

В этом учебнике говорится следующее:

приобретение блокировки подразумевает получение семантики, в то время как релиз блокировки подразумевает семантику выпуска! Все операции с памятью между ними содержатся внутри сэндвича с небольшим барьером, предотвращая любое нежелательное переупорядочение памяти через границы.

Я предполагаю, что приведенная выше цитата говорит о переупорядочении процессора, а не о переупорядочении компилятора.

Но я не понимаю, как блокировка и разблокировка мьютекса заставляет ЦП предоставлять эти функции для получения и выпуска семантики.

Например, если у нас есть следующий код C:

pthread_mutex_lock(&lock); i = 10; j = 20; pthread_mutex_unlock(&lock); 

Вышеприведенный код C преобразуется в следующие (псевдо) инструкции сборки:

 push the address of lock into the stack call pthread_mutex_lock() mov 10 into i mov 20 into j push the address of lock into the stack call pthread_mutex_unlock() 

Теперь, что мешает CPU переупорядочить mov 10 into i и mov 20 into j выше call pthread_mutex_lock() или ниже call pthread_mutex_unlock() ?

Если это команда call которая мешает процессору выполнять переупорядочение, то почему в учебнике, которое я цитировал, кажется, что это функции блокировки и разблокировки мьютексов, которые препятствуют переупорядочению процессора, почему в данном учебном пособии не сказано, что любой вызов функции будет препятствовать переупорядочению ЦП?

Мой вопрос касается архитектуры x86.

Короткий ответ заключается в том, что тело вызовов pthread_mutex_lock и pthread_mutex_unlock будет включать в себя необходимые барьеры для платформы, которые будут препятствовать тому, чтобы процессор перемещал доступ к памяти в критическом разделе вне его. Поток команд будет перемещаться из вызывающего кода в функции lock и unlock помощью команды call , и именно эту динамическую командную трассировку вы должны учитывать для целей переупорядочения, а не для статической последовательности, которую вы видите в списке сборок.

В частности, для x86 вы, вероятно, не найдете явных, автономных барьеров памяти внутри этих методов, так как у вас уже есть lock префиксы для того, чтобы выполнять фактическую блокировку и разблокировку атомарно, и эти инструкции подразумевают полный барьер памяти, который предотвращает переупорядочение процессора, о котором вы беспокоитесь.

Например, в моей системе Ubuntu 16.04 с glibc 2.23, pthread_mutex_lock реализуется с использованием lock cmpxchg (сравнение и обмен), а pthread_mutex_unlock реализуется с использованием lock dec (декремент), оба из которых имеют полную барьерную семантику.

Если i и j – локальные переменные, ничего. Компилятор может хранить их в регистрах через вызов функции, если он может доказать, что ничего вне текущей функции не имеет своего адреса.

Но любые глобальные переменные или местные жители, чей адрес может быть сохранен в глобальной сети, должны быть «синхронизированы» в памяти для вызова не встроенной функции. Компилятор должен предположить, что любая функция, вызывающая его, не может встроить, модифицирует любую / любую переменную, на которую возможно ссылаться.

Так, например, если int i; – локальная переменная, после scanf("0", "%d", &i); его адрес ускользнет от функции, и компилятор затем должен будет проливать / перезагрузить его вокруг вызовов функций, а не сохранять их в регистре, сохраненном для вызовов.

См. Мой ответ в разделе « Понимать переменную volatile asm vs volatile» с примером asm volatile("":::"memory") являющейся барьером для локальной переменной, адрес которой избежал функции ( scanf("0", "%d", &i); ), но не для местных жителей, которые по-прежнему являются чисто локальными. Это точно такое же поведение по той же причине.


Я предполагаю, что приведенная выше цитата говорит о переупорядочении процессора, а не о переупорядочении компилятора.

Это говорит об обоих, потому что оба необходимы для правильности.

Вот почему компилятор не может переупорядочивать обновления для общих переменных с помощью любого вызова функции. (Это очень важно: слабая модель памяти C11 позволяет много переупорядочивать во время компиляции . Сильная модель памяти x86 позволяет только переупорядочивать StoreLoad и локальную пересылку данных.)

pthread_mutex_lock являющийся вызовом не встроенной функции, заботится о переупорядочении времени компиляции , а тот факт, что он выполняет операцию lock , атомный RMW, также означает, что он включает полный барьер памяти во время выполнения на x86. (Не сама инструкция call , а только код в теле функции.) Это дает ему возможность получить семантику.

Для разблокировки спин-блокировки требуется только хранилище-релиз, а не RMW, поэтому в зависимости от деталей реализации функция разблокировки может не быть барьером StoreLoad. (Это все еще в порядке: он не позволяет все в критической секции выйти из системы. Нет необходимости останавливать более поздние операции до появления разблокировки. См. Статью Джеффа Прешинга, объясняющую семантику приобретения и выпуска )

В слабо упорядоченном ISA эти функции мьютекса будут выполнять барьерные инструкции, такие как ARM dmb (барьер памяти данных) . Нормальных функций не было бы, поэтому автор этого руководства правильно указал, что эти функции являются особенными.


Теперь, что мешает CPU переупорядочить mov 10 в i и mov 20 в j выше call pthread_mutex_lock()

Это не важная причина (потому что на слабо упорядоченном ISA pthread_mutex_unlock будет выполняться инструкция по барьеру), но на самом деле это правда на x86, что магазины не могут даже переупорядочиваться с помощью команды call , не говоря уже о фактической блокировке / разблокировке из мьютекса, выполняемого телом функции до возвращения функции.

x86 имеет сильную семантику упорядочения памяти (магазины не переупорядочиваются с другими магазинами), а call – это хранилище (нажатие обратного адреса).

Поэтому mov [i], 10 должно появиться в глобальном хранилище между магазинами, выполненными командой call .

Конечно, в обычной программе никто не наблюдает стек вызовов других streamов, просто xchg чтобы взять мьютекс или хранилище релизов, чтобы освободить его в pthread_mutex_unlock .