webentwicklung-frage-antwort-db.com.de

Was bedeutet die Anweisung "lock" in der x86-Assembly?

Ich habe eine x86-Assembly in Qts Quelle gesehen:

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. Von Googeln wusste ich, dass der Befehl lock die CPU veranlasst, den Bus zu sperren, aber ich weiß nicht, wann die CPU den Bus freigibt?

  2. Über den gesamten obigen Code verstehe ich nicht, wie dieser Code das Add implementiert?

53
gemfield

Was Sie möglicherweise nicht verstehen, ist, dass für den zum Inkrementieren eines Werts erforderlichen Mikrocode der alte Wert zuerst eingelesen werden muss.

Das Schlüsselwort Lock erzwingt, dass die mehreren Mikrobefehle, die tatsächlich auftreten, atomar zu funktionieren scheinen.

Wenn zwei Threads versucht haben, dieselbe Variable zu inkrementieren, und beide gleichzeitig denselben ursprünglichen Wert lesen, werden beide auf denselben Wert inkrementiert und beide schreiben denselben Wert aus.

Anstatt die Variable zweimal inkrementieren zu lassen, was die typische Erwartung ist, wird die Variable am Ende einmal inkrementiert.

Das Schlüsselwort lock verhindert dies.

12
Dan

Von Google wusste ich, dass Sperrbefehle dazu führen, dass die CPU den Bus sperrt, aber ich weiß nicht, wann die CPU den Bus freigibt?

LOCK ist ein Anweisungspräfix, daher gilt es nur für die folgende Anweisung, die Quelle macht es hier nicht sehr deutlich, aber die echte Anweisung ist LOCK INC. Der Bus wird also für das Inkrement gesperrt und dann entsperrt

Über den gesamten obigen Code verstehe ich nicht, wie dieser Code das Add?

Sie implementieren kein Add, sondern ein Inkrement und eine Rückgabe, wenn der alte Wert 0 ist. Ein Addition würde LOCK XADD Verwenden (Windows InterlockedIncrement/Decrement wird jedoch auch mit LOCK XADD).

10
Necrolis

Minimal ausführbare C++ - Threads + LOCK-Inline-Assembly-Beispiel

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 upstream .

Kompilieren und ausführen:

g++ -ggdb3 -O0 -std=c++11 -Wall -Wextra -pedantic -o main.out main.cpp -pthread
./main.out 2 10000

Mögliche Ausgabe:

my_non_atomic_ulong 15264
my_Arch_non_atomic_ulong 15267

Daraus ersehen wir, dass das LOCK-Präfix die Addition atomar gemacht hat: Ohne dieses Präfix haben wir für viele Additionen Race-Bedingungen, und die Gesamtzahl am Ende ist geringer als die synchronisierten 20000.

Siehe auch: Wie sieht die Multicore-Assemblersprache aus?

Getestet unter Ubuntu 19.04 AMD64.