“ lock”指令在 x86汇编中是什么意思?

我在 Qt 的源代码中看到了一些 x86组件:

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
  1. 从谷歌,我知道 lock指令将导致 CPU 锁定总线,但我不知道什么时候 CPU 释放总线?

  2. 关于整个上面的代码,我不明白这个代码是如何实现 Add的?

59605 次浏览

从谷歌,我知道锁定指令将导致 CPU 锁定总线,但我 不知道中央处理器什么时候释放总线?

LOCK是一个指令前缀,因此它只适用于下面的指令,源代码在这里没有说得很清楚,但是真正的指令是 LOCK INC。因此,总线被锁定为增量,然后解锁

关于整个上面的代码,我不明白这些代码 实施加?

它们没有实现 Add,而是实现了一个增量,如果旧值为0,还会返回一个返回指示。另外一个应该使用 LOCK XADD(然而,Windows InterlockedIncrement/Decment 也是用 LOCK XADD实现的)。

  1. LOCK本身不是指令: 它是一个指令前缀,适用于以下指令。这条指令必须是在内存(INCXCHGCMPXCHG等等)上执行读-修改-写操作的指令——在这种情况下,就是 incl (%ecx)指令,incecx寄存器中保存的地址处对 long 单词进行读-修改-写操作。

    LOCK前缀确保 CPU 在操作期间对适当的缓存线路拥有独占所有权,并提供某些额外的排序保证。这可以通过断言总线锁来实现,但 CPU 将尽可能避免这种情况。如果总线被锁定,那么它只在锁定指令期间有效。

  2. 这段代码将要从堆栈中递增的变量的地址复制到 ecx寄存器中,然后执行 lock incl (%ecx)将该变量自动递增1。如果变量的新值为0,那么接下来的两条指令将 eax寄存器(保存函数的返回值)设置为0,否则设置为1。该操作是一个 增量,而不是一个 add (因此得名)。

您可能没有理解的是,递增一个值所需的微代码要求我们首先读入旧值。

Lock 关键字强制实际发生的多个微指令以原子方式进行操作。

如果有两个线程各自尝试增加同一个变量,并且它们都在同一时间读取相同的原始值,那么它们都增加到相同的值,并且它们都写出相同的值。

不是让变量增加两次(这是典型的期望) ,而是最终增加变量一次。

Lock 关键字可以防止这种情况发生。

最小可运行 C + + 线程 + 锁内联程序集示例

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;
}

GitHub 上游。

编译并运行:

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 前缀用于实现:

参见: 多核汇编语言是什么样子的?

在 Ubuntu 19.04 amd64中测试。