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

问题描述 投票:0回答:4

我在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

c++ qt assembly x86
4个回答
133
投票
  1. LOCK
    本身不是指令:它是指令前缀,适用于后面的指令。该指令必须是对内存执行读取-修改-写入操作的指令(
    INC
    XCHG
    CMPXCHG
    等)——在本例中,它是
    incl (%ecx)
    指令,它会
    inc
    重新记录
     l
    长字位于
    ecx
    寄存器中保存的地址。

    LOCK
    前缀确保CPU在操作期间拥有相应缓存行的独占所有权,并提供某些额外的排序保证。这可以通过断言总线锁定来实现,但 CPU 将尽可能避免这种情况。如果总线被锁定,则仅在锁定指令的持续时间内锁定。

  2. 此代码将要从堆栈递增的变量的地址复制到

    ecx
    寄存器中,然后
    lock incl (%ecx)
    以原子方式将该变量递增 1。接下来的两条指令设置
    eax
    寄存器(其中保存如果变量的新值为 0,则函数的返回值)为 0,否则为 1。该操作是增量,而不是添加(因此得名)。


17
投票

增加值所需的微代码要求我们首先读入旧值。

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

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

您最终不会将变量递增两次(这是典型的期望),而是将变量递增一次。

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


15
投票

从谷歌,我知道锁定指令会导致CPU锁定总线,但我 不知道cpu什么时候释放总线?

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

关于上面的整个代码,我不明白这些代码是如何实现的 实施了添加吗?

它们不实现添加,而是实现增量,以及如果旧值为 0 则返回指示。加法将使用

LOCK XADD
(但是,Windows InterlockedIncrement/Decrement 也使用
LOCK XADD
实现)。


4
投票

最小可运行 C++ 线程 + LOCK 内联汇编示例

主.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 中测试。

© www.soinside.com 2019 - 2024. All rights reserved.