Что означает инструкция "lock" в сборке x86?
Я видел некоторую сборку x86 в источнике Qt:
q_atomic_increment:
movl 4(%esp), %ecx
lock
incl (%ecx)
mov $0,%eax
setne %al
ret
.align 4,0x90
.type q_atomic_increment,@function
.size q_atomic_increment,.-q_atomic_increment
От Google, я знал,
lock
инструкция заставит процессор блокировать шину, но я не знаю, когда процессор освобождает шину?Что касается всего приведенного выше кода, я не понимаю, как этот код реализует
Add
?
3 ответа
LOCK
это не сама инструкция: это префикс инструкции, который применяется к следующей инструкции. Эта инструкция должна быть чем-то, что делает чтение-изменение-запись в памяти (INC
,XCHG
,CMPXCHG
и т.д.) --- в данном случае этоincl (%ecx)
инструкция, котораяinc
возражаетl
слово по адресу, указанному вecx
регистр.LOCK
Префикс гарантирует, что ЦП обладает исключительным владением соответствующей строкой кэша на время операции, и обеспечивает некоторые дополнительные гарантии упорядочения. Этого можно добиться, установив блокировку шины, но ЦПУ по возможности избежит этого. Если шина заблокирована, то это только на время заблокированной инструкции.Этот код копирует адрес переменной для увеличения из стека в
ecx
зарегистрируйтесь, тогда этоlock incl (%ecx)
атомарно увеличить эту переменную на 1. Следующие две инструкции устанавливаютeax
регистр (который содержит возвращаемое значение из функции) в 0, если новое значение переменной равно 0, и 1 в противном случае. Операция является приращением, а не добавлением (отсюда и название).
Возможно, вы не понимаете, что микрокод, необходимый для увеличения значения, требует, чтобы мы сначала прочитали старое значение.
Ключевое слово Lock заставляет несколько микро-команд, которые фактически появляются, работать атомарно.
Если у вас было 2 потока, каждый из которых пытался увеличить одну и ту же переменную, и они оба считывали одно и то же исходное значение в одно и то же время, они оба увеличивали до одного и того же значения, и оба записывали одно и то же значение.
Вместо того, чтобы увеличивать переменную дважды, что является типичным ожиданием, вы заканчиваете тем, что увеличиваете переменную один раз.
Ключевое слово lock предотвращает это.
От Google я знал, что инструкция блокировки приведет к тому, что процессор заблокирует шину, но я не знаю, когда процессор освободит автобус?
LOCK
является префиксом инструкции, следовательно, он применяется только к следующей инструкции, здесь источник не очень понятен, но настоящая инструкция LOCK INC
, Таким образом, шина заблокирована для приращения, затем разблокирована
Что касается всего приведенного выше кода, я не понимаю, как этот код реализовал Add?
Они не реализуют Add, они реализуют инкремент, вместе с указанием возврата, если старое значение было 0. Дополнение будет использовать LOCK XADD
(однако окна InterlockedIncrement/Decrement также реализуются с LOCK XADD
).
Минимальные исполняемые потоки C++ + пример встроенной сборки LOCK
main.cpp
#include <atomic>
#include <cassert>
#include <iostream>
#include <thread>
#include <vector>
std::atomic_ulong my_atomic_ulong(0);
unsigned long my_non_atomic_ulong = 0;
unsigned long my_arch_atomic_ulong = 0;
unsigned long my_arch_non_atomic_ulong = 0;
size_t niters;
void threadMain() {
for (size_t i = 0; i < niters; ++i) {
my_atomic_ulong++;
my_non_atomic_ulong++;
__asm__ __volatile__ (
"incq %0;"
: "+m" (my_arch_non_atomic_ulong)
:
:
);
__asm__ __volatile__ (
"lock;"
"incq %0;"
: "+m" (my_arch_atomic_ulong)
:
:
);
}
}
int main(int argc, char **argv) {
size_t nthreads;
if (argc > 1) {
nthreads = std::stoull(argv[1], NULL, 0);
} else {
nthreads = 2;
}
if (argc > 2) {
niters = std::stoull(argv[2], NULL, 0);
} else {
niters = 10000;
}
std::vector<std::thread> threads(nthreads);
for (size_t i = 0; i < nthreads; ++i)
threads[i] = std::thread(threadMain);
for (size_t i = 0; i < nthreads; ++i)
threads[i].join();
assert(my_atomic_ulong.load() == nthreads * niters);
assert(my_atomic_ulong == my_atomic_ulong.load());
std::cout << "my_non_atomic_ulong " << my_non_atomic_ulong << std::endl;
assert(my_arch_atomic_ulong == nthreads * niters);
std::cout << "my_arch_non_atomic_ulong " << my_arch_non_atomic_ulong << std::endl;
}
Скомпилируйте и запустите:
g++ -ggdb3 -O0 -std=c++11 -Wall -Wextra -pedantic -o main.out main.cpp -pthread
./main.out 2 10000
Возможный выход:
my_non_atomic_ulong 15264
my_arch_non_atomic_ulong 15267
Из этого мы видим, что префикс LOCK сделал добавление атомарным: без него у нас есть условия гонки для многих добавлений, а общее количество в конце меньше, чем синхронизированное 20000.
Префикс LOCK используется для реализации:
- C++11
std::atomic
: Что такое std:: atomic? - C11
atomic_int
: Как мне запустить потоки на простом C?
См. Также: Как выглядит многоядерный ассемблер?
Протестировано в Ubuntu 19.04 amd64.