Çoklu iş parçacığı programı en iyi duruma getirilmiş durumda kalmış ancak normalde -O0'da çalışıyor


68

Basit bir çok iş parçacığı içeren programlar yazdım:

static bool finished = false;

int func()
{
    size_t i = 0;
    while (!finished)
        ++i;
    return i;
}

int main()
{
    auto result=std::async(std::launch::async, func);
    std::this_thread::sleep_for(std::chrono::seconds(1));
    finished=true;
    std::cout<<"result ="<<result.get();
    std::cout<<"\nmain thread id="<<std::this_thread::get_id()<<std::endl;
}

Bu hata ayıklama modunda normal davranır Görsel stüdyoda veya -O0içinde gc c ve sonra sonucu çıktısını 1saniye. Ancak yapışmış ve Release modunda veya hiçbir şey yazdırmıyor -O1 -O2 -O3.


Yorumlar uzun tartışmalar için değildir; bu sohbet sohbete taşındı .
Samuel Liew

Yanıtlar:


100

Olmayan bir atom olmayan korunan değişkeni erişen iki konu vardır UB Bu endişeler finished. Bunu düzeltmek için finishedtür yapabilirsiniz std::atomic<bool>.

Düzeltmem:

#include <iostream>
#include <future>
#include <atomic>

static std::atomic<bool> finished = false;

int func()
{
    size_t i = 0;
    while (!finished)
        ++i;
    return i;
}

int main()
{
    auto result=std::async(std::launch::async, func);
    std::this_thread::sleep_for(std::chrono::seconds(1));
    finished=true;
    std::cout<<"result ="<<result.get();
    std::cout<<"\nmain thread id="<<std::this_thread::get_id()<<std::endl;
}

Çıktı:

result =1023045342
main thread id=140147660588864

Coliru'da Canlı Demo


Birisi 'Bu bir bool- muhtemelen bir bit. Bu nasıl atomik olmayabilir? ' (Kendime çoklu iplik geçirmeye başladığımda yaptım.)

Ancak, gözyaşı eksikliğinin std::atomicsize veren tek şey olmadığını unutmayın. Ayrıca, derleyicinin değişkeni yeniden okumanın her zaman aynı değeri göreceğini varsaymasını engelleyerek, birden çok iş parçacığından eşzamanlı okuma + yazma erişimi sağlar.

boolKorunmasız, atomik olmayan yapmak ek sorunlara neden olabilir:

  • Derleyici, değişkeni bir sicile ve hatta CSE çoklu erişimini bir sınıfa optimize etmeye ve bir döngüden bir yükü kaldırmaya karar verebilir.
  • Değişken bir CPU çekirdeği için önbelleğe alınabilir. (Gerçek hayatta, CPU'lar tutarlı önbellekleri sahiptir . Bu gerçek bir sorun değildir, ancak C ++ standart ++ uyumlu olmayan ortak bellek uygulamalarını varsayımsal C kapağı için gevşek yeterli atomic<bool>olan memory_order_relaxedmağaza / yük çalışması, ancak burada volatileolmaz. Kullanılması gerçek C ++ uygulamaları üzerinde pratikte çalışmasına rağmen, bunun için uçucu olan UB olurdu.)

Bunun olmasını önlemek için derleyiciye açıkça yapmamaları söylenmelidir.


volatileBu konunun potansiyel ilişkisi ile ilgili gelişen tartışma hakkında biraz şaşırdım . Böylece, iki sentimi harcamak istiyorum:


4
Bir göz attım func()ve "Bunu uzatabilirim " diye düşündüm . Optimiser dişleri hiç umursamıyor ve sonsuz döngüyü tespit edecek ve eğer mutlu bir şekilde "while (True)" ya çevirirsek .org / z / Tl44iN bunu görebiliriz. Eğer bitti ise Truegeri döner. Değilse, etikette koşulsuz bir zıplamaya (sonsuz bir döngü) girer.L5
Baldrickk


2
@val: volatileC ++ 11'de kötüye kullanmak için temelde hiçbir neden yoktur , çünkü atomic<T>ve ile aynı özdeşliği elde edebilirsiniz std::memory_order_relaxed. Gerçek donanımda da çalışır: önbellekler uyumludur, bu nedenle başka bir çekirdekteki bir mağaza önbelleğe alma taahhüdünde bulunduğunda bir yükleme talimatı eski bir değeri okumaya devam edemez. (MESI)
Peter Cordes

5
@PeterCordes volatileYine de UB kullanmaktır . Kesinlikle ve açıkça bir şey olduğunu asla varsaymamalısınız UB sadece yanlış gidebileceği bir yol düşünemiyorum ve denediğinizde işe yaradı. Bu insanları tekrar tekrar yaktı.
David Schwartz

2
@Damon Mutexes semantiği serbest bırakır / alır. Bir muteks daha önce kilitlenmişse derleyicinin okumayı optimize etmesine izin verilmez, bu nedenle finishedbir std::mutexişle ( volatileveya olmadan atomic) koruma . Aslında, tüm atomları "basit" değer + muteks şeması ile değiştirebilirsiniz; hala işe yarayacak ve sadece daha yavaş olacaktı. atomic<T>dahili bir muteks kullanma izni vardır; sadece atomic_flagkilitsiz olarak garanti edilir.
Erlkoenig

42

Scheff'ın cevabı kodunuzu nasıl düzelteceğinizi açıklar. Bu durumda gerçekte neler olduğuna dair biraz bilgi ekleyeceğimi düşündüm.

Optimizasyon seviyesi 1 ( ) kullanarak kodunuzu godbolt'ta derledim-O1 . İşleviniz şöyle derlenir:

func():
  cmp BYTE PTR finished[rip], 0
  jne .L4
.L5:
  jmp .L5
.L4:
  mov eax, 0
  ret

Peki, burada neler oluyor? İlk olarak, bir karşılaştırmamız var: cmp BYTE PTR finished[rip], 0- Bu finished, yanlış olup olmadığını kontrol eder .

Eğer öyleyse değil yanlış (aka true) biz ilk çalıştırmada döngü çıkmak gerekir. Bu gerçekleştirilir göre jne .L4olan j umps N ot e etikete Qual .L4değeri burada i( 0) daha sonra kullanmak ve ve fonksiyon için bir kayıt saklanır.

O takdirde ise ancak yanlış, biz taşımak

.L5:
  jmp .L5

Bu, .L5atlama komutunun kendisi olan etiketlemek için koşulsuz bir atlamadır .

Başka bir deyişle, iş parçacığı sonsuz meşgul döngüsüne konur.

Peki bu neden oldu?

İyileştirici ile ilgili olarak, dişler tasarımının dışındadır. Diğer iş parçacıklarının eşzamanlı olarak değişkenleri okumadığını veya yazmadığını varsayar (çünkü bu, veri yarışı UB olacaktır). Erişimleri optimize edemeyeceğini söylemelisiniz. Scheff'in cevabı burada devreye giriyor. Onu tekrarlamak için uğraşmayacağım.

Optimize ediciye, finisheddeğişkenin işlevin yürütülmesi sırasında potansiyel olarak değişebileceği söylenmediğinden finished, işlevin kendisi tarafından değiştirilmediğini görür ve sabit olduğunu varsayar.

Optimize edilmiş kod, işleve sabit bir bool değeriyle girilmesinden kaynaklanacak iki kod yolunu sağlar; ya döngüyü sonsuz olarak çalıştırır ya da döngü asla çalıştırılmaz.

en -O0(beklendiği gibi) derleyici uzaklıkta halka gövdesi ve karşılaştırma optimize etmez:

func():
  push rbp
  mov rbp, rsp
  mov QWORD PTR [rbp-8], 0
.L148:
  movzx eax, BYTE PTR finished[rip]
  test al, al
  jne .L147
  add QWORD PTR [rbp-8], 1
  jmp .L148
.L147:
  mov rax, QWORD PTR [rbp-8]
  pop rbp
  ret

bu nedenle fonksiyon, optimize edilmediğinde, burada atomisite eksikliği tipik olarak bir problem değildir, çünkü kod ve veri tipi basittir. Muhtemelen burada karşılaşabileceğimiz en kötü, bir olması gereken işey tarafından kapalı bir değerdir .

Veri yapılarına sahip daha karmaşık bir sistemin bozuk veri veya yanlış yürütme ile sonuçlanma olasılığı daha yüksektir.


3
C ++ 11, iş parçacıkları ve iş parçacığına duyarlı bir bellek modelini dilin kendisinin bir parçası yapar. Bu, derleyicilerin atomickoddaki bu değişkenleri yazmayan değişken olmayanlara bile yazma icat edemediği anlamına gelir . Örneğin if (cond) foo=1;, foo = cond ? 1 : foo;load + store (atomik bir RMW değil) başka bir iş parçacığından yazma işlemine basabileceğinden asm'ye dönüştürülemez. Derleyiciler zaten çok iş parçacıklı programlar yazmak için yararlı olmak istedikleri için böyle şeylerden kaçınıyorlardı, ancak C ++ 11 derleyicilerin 2 iş parçacığının yazdığı kodu kırmaması gerektiğini resmi hale getirdi a[1]vea[2]
Peter Cordes

2
Ama evet, derleyiciler parçacığı farkında değildir nasıl o abartı dışındaki hiç , cevabın doğru. Veri yarışı UB, küreseller de dahil olmak üzere atom olmayan değişkenlerin kaldırma yüklerine ve tek iş parçacıklı kod için istediğimiz diğer agresif optimizasyonlara izin verir. MCU programlama - C ++ O2 optimizasyonu elektronik devrede kırılır.SE bu açıklamanın benim versiyonum.
Peter Cordes

1
@PeterCordes: Bir GC kullanarak Java'nın bir avantajı, nesneler için belleğin eski ve yeni kullanım arasında araya giren bir küresel bellek engeli olmadan geri dönüştürülmemesidir , yani bir nesneyi inceleyen herhangi bir çekirdek her zaman sahip olduğu bir değer görecektir referans ilk yayınlandıktan sonra bir süre tutuldu. Global bellek bariyerleri sık kullanılırlarsa çok pahalı olsa da, az miktarda kullanıldıklarında bile başka yerlerde bellek bariyerlerine olan ihtiyacı büyük ölçüde azaltabilirler.
supercat

1
Evet, bunu söylemeye çalıştığınızı biliyordum, ama% 100 ifadenizin bu anlama geldiğini sanmıyorum. Optimize ediciyi "tamamen yok sayar." tam olarak doğru değil: optimizasyon sırasında iş parçacığını gerçekten görmezden gelmenin, kelime yükü / kelime deposunda bir bayt değiştirme gibi şeyleri içerebileceği iyi bilinmektedir, bu da pratikte bir iş parçacığının bir karakter veya bitfield adımlarına eriştiği hatalara neden olmuştur. bitişik bir yapı üyesine yaz. Tüm hikaye için lwn.net/Articles/478657'ye bakın ve sadece C11 / C ++ 11 bellek modelinin böyle bir optimizasyonu nasıl pratikte değil, sadece yasadışı hale getirdiğini görün.
Peter Cordes

1
Hayır, bu iyi .. Teşekkürler @PeterCordes. Gelişmeyi takdir ediyorum.
Baldrickk

5

Öğrenme eğrisindeki bütünlük uğruna; global değişkenleri kullanmaktan kaçınmalısınız. Statik yaparak iyi bir iş çıkardınız, bu yüzden çeviri birimi için yerel olacak.

İşte bir örnek:

class ST {
public:
    int func()
    {
        size_t i = 0;
        while (!finished)
            ++i;
        return i;
    }
    void setFinished(bool val)
    {
        finished = val;
    }
private:
    std::atomic<bool> finished = false;
};

int main()
{
    ST st;
    auto result=std::async(std::launch::async, &ST::func, std::ref(st));
    std::this_thread::sleep_for(std::chrono::seconds(1));
    st.setFinished(true);
    std::cout<<"result ="<<result.get();
    std::cout<<"\nmain thread id="<<std::this_thread::get_id()<<std::endl;
}

Wandbox'ta canlı


1
Ayrıca fonksiyon bloğu içinde olduğu finishedgibi beyan edilebilir static. Yine de yalnızca bir kez başlatılacak ve bir sabit değere başlatılırsa, kilitleme gerektirmez.
Davislor

Erişim finisheddaha ucuz std::memory_order_relaxedyük ve depolar kullanabilirsiniz; wrt gerekli sipariş yok. her iki iş parçacığında diğer değişkenler. @ Davislor'un önerisinin staticmantıklı olduğundan emin değilim ; birden fazla spin sayma iş parçacığına sahip olsaydınız hepsini aynı bayrakla durdurmak istemezsiniz. finishedBaşlatılmasını, atomik bir mağazaya değil, sadece başlatmaya derleyecek şekilde yazmak istiyorsunuz . ( finished = false;Varsayılan başlatıcı C ++ 17 sözdizimi ile yaptığınız gibi . Godbolt.org/z/EjoKgq ).
Peter Cordes

@PeterCordes Bayrağı bir nesneye koymak, sizin gibi, farklı iş parçacığı havuzları için birden fazla yer olmasına izin verir. Ancak orijinal tasarımın tüm iş parçacıkları için tek bir bayrağı vardı.
Davislor
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.