Program ini kadang-kadang akan mencetak 00, tetapi jika saya mengomentari A.Store dan B.Store dan UNCOMMENT A.FETH_ADD dan B.FETH_ADD yang melakukan hal yang sama persis yaitu menetapkan nilai A = 1, B = 1, saya tidak pernah melakukannya Dapatkan 00 . (Diuji pada Intel i3 x86-64, dengan G ++ -O2)

Apakah saya kehilangan sesuatu, atau dapat "00" tidak pernah terjadi dengan standar?

Ini adalah versi dengan toko biasa, yang dapat mencetak 00.

// g++ -O2 -pthread axbx.cpp  ; while [ true ]; do ./a.out  | grep "00" ; done
#include<cstdio>
#include<thread>
#include<atomic>
using namespace std;
atomic<int> a,b;
int reta,retb;

void foo(){
        //a.fetch_add(1,memory_order_relaxed);
        a.store(1,memory_order_relaxed);
        retb=b.load(memory_order_relaxed);
}

void bar(){
        //b.fetch_add(1,memory_order_relaxed);
        b.store(1,memory_order_relaxed);
        reta=a.load(memory_order_relaxed);
}

int main(){
        thread t[2]{ thread(foo),thread(bar) };
        t[0].join(); t[1].join();
        printf("%d%d\n",reta,retb);
        return 0;
}

Di bawah ini tidak pernah mencetak 00

// g++ -O2 -pthread axbx.cpp  ; while [ true ]; do ./a.out  | grep "00" ; done
#include<cstdio>
#include<thread>
#include<atomic>
using namespace std;
atomic<int> a,b;
int reta,retb;

void foo(){
        a.fetch_add(1,memory_order_relaxed);
        //a.store(1,memory_order_relaxed);
        retb=b.load(memory_order_relaxed);
}

void bar(){
        b.fetch_add(1,memory_order_relaxed);
        //b.store(1,memory_order_relaxed);
        reta=a.load(memory_order_relaxed);
}

int main(){
        thread t[2]{ thread(foo),thread(bar) };
        t[0].join(); t[1].join();
        printf("%d%d\n",reta,retb);
        return 0;
}

Lihat ini juga Multithreading Atomics AB Printing 00 untuk memori_order_relax

3
nvn 5 April 2021, 16:27

3 jawaban

Jawaban Terbaik

standar memungkinkan 00, tetapi Anda tidak akan pernah mendapatkannya pada x86 karena satu-satunya cara untuk mengimplementasikan atom RMW pada x86 melibatkan lock awalan, yang merupakan "penghalang penuh" ", yang cukup kuat untuk seq_cst.

Dalam istilah C ++, atom RMW secara efektif dipromosikan ke seq_cst ketika dikompilasi untuk x86 . (Hanya setelah kemungkinan pemesanan waktu kompilasi dipaku - mis. Beban / toko non-atom dapat menyusun ulang / menggabungkan melintasi fetch_add yang santai)

Bahkan, sebagian besar kompiler mengimplementasikan a.store(1, mo_seq_cst) dengan menggunakan xchg (yang memiliki lock implisit), karena lebih cepat dari mov + mfence pada CPU modern. Fakta menyenangkan: dengan hanya menyimpan dan memuat, kode Anda akan dikompilasi dengan ASM yang sama dengan https://preshing.com/20120515/memory-reordering-caught-the-act/.


ISO C ++ memungkinkan seluruh RMW rileks untuk menyusun ulang dengan beban santai, tetapi kompiler normal tidak akan melakukan itu pada waktu kompilasi tanpa alasan. (Implementasi deathstation 9000 c ++ bisa / akan). Jadi akhirnya Anda menemukan kasus di mana akan berguna untuk menguji pada ISA yang berbeda. Cara-cara di mana RMW atom (atau bagian itu) dapat memesan ulang sangat tergantung pada ISA.


Sebuah ll / sc mesin yang membutuhkan loop coba lagi untuk mengimplementasikan fetch_add ( Misalnya lengan, atau Aarch64 sebelum ARMV8.1) mungkin dapat benar-benar menerapkan RMW yang rileks yang dapat menyusun ulang saat run-time karena sesuatu yang lebih kuat daripada santai akan membutuhkan hambatan. (Atau memperoleh / merilis versi instruksi seperti {x0-load {{x0) }} / stlxr vs. ldxr / stxr). Jadi jika ada perbedaan ASM antara santai dan ACQ dan / atau rel (dan kadang-kadang SEQ_CST juga berbeda), kemungkinan perbedaan diperlukan dan mencegah pemesanan ulang waktu run-time.

Bahkan operasi atom instruksi tunggal mungkin dapat benar-benar santai di Aarch64; Saya belum menyelidiki. Paling lemah yang dipesan oleh ISA secara tradisional menggunakan atomik LL / SC, jadi saya mungkin akan menyatukannya.

Dalam mesin LL / SC, sisi toko RMW LL / SC bahkan dapat menyusun ulang dengan beban nanti secara terpisah dari beban, kecuali mereka berdua SEQ_CST. untuk tujuan pemesanan, adalah Atom Read-Modify-Tulis satu atau dua operasi?


Untuk benar-benar melihat 00, kedua beban harus terjadi sebelum bagian toko dari RMW terlihat di utas lainnya. Dan ya, mekanisme pemesanan ulang HW dalam mesin LL / SC akan saya pikir cukup mirip dengan menyusun ulang toko biasa.

3
Peter Cordes 5 April 2021, 22:15

Kuncinya dalam pertanyaan ini adalah untuk menyadari bahwa memesan memori santai Tidak ada jaminan sinkronisasi antara utas:

Operasi atom yang ditandai memori_order_relax bukan operasi sinkronisasi; Mereka tidak memaksakan pesanan di antara akses memori bersamaan . Mereka hanya menjamin atomicity dan konsistensi pemesanan modifikasi.

Jadi dalam kode pertama, skenario yang berbeda bisa terjadi. Sebagai contoh:

  • Kode di utas untuk foo() dijalankan terlebih dahulu, kemudian utas untuk bar(): retb adalah 0, reta adalah 1 jadi Anda akan mendapatkan 10.
  • kode di utas untuk bar() dijalankan terlebih dahulu, kemudian utas untuk foo(): reta adalah 0, retb adalah 1 jadi Anda akan mendapatkan 01.
  • Kode di utas untuk foo() dan bar() dieksekusi instruksi dengan instruksi secara bersamaan. Kemudian reta dan retb akan keduanya 1, dan Anda akan mendapatkan 11.
  • Memesan memori yang santai juga memungkinkan untuk situasi yang tidak disinkronkan: di mana kedua utas memperbarui atom mereka dan melihat nilai atom mereka saat ini, tetapi melihat nilai yang tidak disusun dari utas lainnya (I.E. Nilai sebelum perubahan atom). Jadi Anda bisa reta dan retb pada 0 menjemput Anda 00.

Kode kedua menderita masalah yang sama, karena ini adalah pemesanan santai dan akses yang digunakan untuk mengatur reta dan retb hanya membaca akses pada atom yang dimodifikasi di utas lain. Anda bisa memiliki semua keempat kemungkinan.

Jika Anda ingin memastikan sinkronisasi terjadi seperti yang diharapkan, Anda dapat menjaga pemesanan santai untuk operasi penulisan, tetapi memuat nilai menggunakan memory_order_acquire. Ini akan mengesampingkan 00 tetapi masih meninggalkan semua kombinasi lainnya.

-1
Christophe 5 April 2021, 21:36

Saya mendapatkan "10" dalam kedua kasus. Utas pertama akan selalu berjalan lebih cepat dan a == 1! Tetapi jika Anda menambahkan operasi tambahan ke foo()

#include<cstdio>
#include<thread>
#include<atomic>
using namespace std;
atomic<int> a,b;
int reta,retb;

void foo(){

    int i=0;
    while(i < 10000000)
        i++;

    a.fetch_add(1,memory_order_relaxed);
    //a.store(1,memory_order_relaxed);
    retb=b.load(memory_order_relaxed);
}

void bar(){
    b.fetch_add(1,memory_order_relaxed);
    //b.store(1,memory_order_relaxed);
    reta=a.load(memory_order_relaxed);
}

int main(){
    thread t[2]{ thread(foo),thread(bar) };
    t[0].join(); t[1].join();
    printf("%d%d\n",reta,retb);
    return 0;
}

Anda akan menerima "01"!

-1
Serhii Oleksenko 5 April 2021, 14:05