C ++ uçucu anahtar sözcüğü bellek çiti sağlar mı?


86

onu anlıyorum volatile derleyiciye değerin değiştirilebileceği bilgi verdiğini , ancak bu işlevi başarmak için derleyicinin çalışmasını sağlamak için bir bellek çiti eklemesi gerekiyor mu?

Anladığım kadarıyla, uçucu nesneler üzerindeki işlemlerin sırası yeniden düzenlenemez ve korunmalıdır. Bu, bazı hafıza sınırlarının gerekli olduğunu ve bunun gerçekten bir yolu olmadığını ima ediyor gibi görünüyor. Bunu söylemekte haklı mıyım?


Bu ilgili soruda ilginç bir tartışma var

Jonathan Wakely şöyle yazar :

... Farklı uçucu değişkenlere erişim, ayrı tam ifadelerde görüldükleri sürece derleyici tarafından yeniden sıralanamaz ... doğru, uçucu iş parçacığı güvenliği için yararsızdır, ancak verdiği nedenlerden dolayı değildir. Bunun nedeni, derleyicinin geçici nesnelere erişimi yeniden sıralayabilmesi değil, CPU'nun bunları yeniden sıralayabilmesidir. Atomik işlemler ve bellek engelleri, derleyicinin ve CPU'nun yeniden sıralanmasını engeller

Hangi için David Schwartz yanıtlar yorumlarda :

... C ++ standardı açısından, bir şeyler yapan derleyici ile donanımın bir şeyler yapmasına neden olan talimatlar yayınlayan derleyici arasında hiçbir fark yoktur. CPU, erişimleri uçuculara yeniden sıralayabilirse, standart, sıralarının korunmasını gerektirmez. ...

... C ++ standardı, yeniden sıralamanın ne olduğu konusunda herhangi bir ayrım yapmaz. Ve CPU'nun onları gözlemlenebilir bir etki olmadan yeniden sıralayabileceğini iddia edemezsiniz, bu yüzden sorun değil - C ++ standardı, sıralarını gözlemlenebilir olarak tanımlar. Derleyici, platformun standardın gerektirdiklerini yapmasını sağlayan kod üretirse, bir platformdaki C ++ standardıyla uyumludur. Standart, uçucu maddelere erişimlerin yeniden sıralanmamasını gerektiriyorsa, onları yeniden sipariş eden bir platform uyumlu değildir. ...

Demek istediğim, eğer C ++ standardı, derleyicinin, bu tür erişimlerin sırasının programın gözlemlenebilir davranışının bir parçası olduğu teorisine göre, erişimleri farklı uçucu maddelere yeniden düzenlemesini yasaklarsa, derleyicinin de CPU'nun yapmasını engelleyen kod yaymasını gerektirmesidir. yani. Standart, derleyicinin ne yaptığı ile derleyicinin ürettiği kodun CPU'nun yaptığı şeyi ayırt etmez.

Hangisi iki soruyu ortaya çıkarır: İkisi de "doğru" mu? Gerçek uygulamalar gerçekten ne işe yarar?


9
Çoğunlukla derleyicinin bu değişkeni bir kayıt defterinde tutmaması gerektiği anlamına gelir. Kaynak kodundaki her atama ve okuma, ikili koddaki bellek erişimlerine karşılık gelmelidir.
Basile Starynkevitch


1
Değerin dahili bir kayıtta saklanması durumunda herhangi bir bellek çitinin etkisiz olacağından şüpheleniyorum. Eşzamanlı bir durumda yine de başka koruyucu önlemler almanız gerektiğini düşünüyorum.
Galik

Bildiğim kadarıyla, donanım tarafından değiştirilebilen değişkenler için uçucu kullanılıyor (genellikle mikro denetleyicilerle birlikte kullanılır). Basitçe, değişkeni okumak farklı bir sırayla yapılamaz ve optimize edilemez anlamına gelir. Bu C olsa, ancak ++ 'da aynı olmalıdır.
Mast

1
@Mast volatileDeğişkenlerin okunmasını CPU önbellekleri tarafından optimize edilmesini engelleyen bir derleyici henüz görmedim . Ya tüm bu derleyiciler uyumsuzdur ya da standart düşündüğünüz anlamına gelmez. (Standart, derleyicinin ne yaptığı ile derleyicinin CPU'ya ne yaptığı arasında ayrım yapmaz. Çalıştırıldığında standarda uyan kodu yaymak derleyicinin görevidir.)
David Schwartz

Yanıtlar:


58

Neyin volatileişe yaradığını açıklamaktansa, ne zaman kullanmanız gerektiğini açıklamama izin verin volatile.

  • Bir sinyal işleyicinin içindeyken. Çünkü bir volatiledeğişkene yazmak , standardın bir sinyal işleyicinin içinden yapmanıza izin verdiği hemen hemen tek şeydir. C ++ 11'den beri std::atomicbu amaçla kullanabilirsiniz , ancak yalnızca atomik kilitsiz ise.
  • setjmp Intel'e göre iş yaparken .
  • Doğrudan donanımla uğraşırken ve derleyicinin okumalarınızı veya yazmalarınızı optimize etmediğinden emin olmak istiyorsanız.

Örneğin:

volatile int *foo = some_memory_mapped_device;
while (*foo)
    ; // wait until *foo turns false

Tanımlayıcı olmadan, volatilederleyicinin döngüyü tamamen optimize etmesine izin verilir. volatileBelirteci o müteakip 2 aynı değeri döndürmek okur varsayalım olmayabilir derleyici söyler.

volatileKonu ile ilgisi olmadığını unutmayın . Yukarıdaki örnek, farklı bir ileti dizisi yazıyorsa işe yaramaz.*foo çünkü ilgili bir alma işlemi yoktur.

Diğer tüm durumlarda, volatileC ++ 11 öncesi derleyiciler ve derleyici uzantıları (örneğin /volatile:ms, X86 / I64 altında varsayılan olarak etkinleştirilen msvc anahtarı gibi) ile ilgilenme haricinde , kullanımının taşınabilir olmadığı ve kod incelemesini geçemediği düşünülmelidir .


5
"Sonraki 2 okumanın aynı değeri döndürdüğünü varsaymayabilir" den daha katıdır. Yalnızca bir kez okusanız ve / veya değerleri atsanız bile, okumanın yapılması gerekir.
philipxy

1
Sinyal işleyicilerinde kullanım setjmpve standart markaların iki garantisidir. Öte yandan, amaç , en azından başlangıçta bellek eşlemeli GÇ'yi desteklemekti. Bazı işlemcilerde çit veya membran gerektirebilir.
James Kanze

@philipxy "Okumanın" ne anlama geldiğini kimse bilmiyor. Örneğin, kimse bellekten gerçek bir okuma yapılması gerektiğine inanmıyor - volatileerişimlerde CPU önbelleklerini atlamaya çalıştığını bildiğim hiçbir derleyici .
David Schwartz

@JamesKanze: Öyle değil. Yeniden sinyal işleyicileri standart, sinyal işleme sırasında yalnızca uçucu std :: sig_atomic_t & kilit içermeyen atomik nesnelerin tanımlanmış değerlere sahip olduğunu söyler. Ancak, uçucu nesnelere erişimin gözlemlenebilir yan etkiler olduğunu da söylüyor.
philipxy

1
@DavidSchwartz Bazı derleyici mimarisi çiftleri, standart olarak belirlenmiş erişim sırasını gerçek efektlere eşler ve çalışma programları bu efektleri elde etmek için uçuculara erişir. Bu tür çiftlerden bazılarının eşleştirmesinin olmaması veya önemsiz, yardımcı olmayan bir eşlemenin olmaması, uygulamaların kalitesiyle ilgilidir, ancak eldeki nokta ile ilgili değildir.
philipxy

25

C ++ uçucu anahtar sözcüğü bellek çiti sağlar mı?

Spesifikasyona uyan bir C ++ derleyicisinin bellek çiti tanıtması gerekmez. Özel derleyiciniz şunları yapabilir; sorunuzu derleyicinizin yazarlarına yönlendirin.

C ++ 'daki "uçucu" işlevinin iş parçacığı ile ilgisi yoktur. Unutmayın, "uçucu" un amacı, derleyici optimizasyonlarını devre dışı bırakmaktır, böylece dışsal koşullar nedeniyle değişen bir kayıt defterinden okuma optimize edilmez. Farklı bir CPU'da farklı bir iş parçacığı tarafından yazılan bir bellek adresi, eksojen koşullar nedeniyle değişen bir kayıt mı? Bazı derleyici yazarlar varsa sayılı Yine, seçilmiş bir muamele bellek adreslerine onlar eksojen koşulları nedeniyle değişen kayıtları sanki kendi iş olduğunu, farklı CPU'lar üzerinde farklı iş parçacıkları tarafından yazıldığını; bunu yapmaları gerekli değildir. Örneğin, her iş parçacığının tutarlı bir şekilde görmesini sağlamak için - bir bellek çiti getirse bile - gerekli değildir. uçucu okuma ve yazma sıralaması.

Aslında, uçucu, C / C ++ 'da iş parçacığı oluşturmak için hemen hemen yararsızdır. En iyi uygulama bundan kaçınmaktır.

Ayrıca: bellek çitleri, belirli işlemci mimarilerinin bir uygulama detayıdır. Açıkça uçucu C #, içinde olduğu program ilk etapta çitleri bulunmayan bir mimaride çalışıyor olabilir çünkü çoklu kullanım için tasarlanmış, yarım çitler söylemez şartname, tanıtılacak. Bunun yerine, yine belirtim, bazı yan etkilerin nasıl sıralanacağına ilişkin belirli (son derece zayıf) kısıtlamalar koymak için derleyici, çalışma zamanı ve CPU tarafından hangi optimizasyonlardan kaçınılacağına dair belirli (son derece zayıf) garantiler sağlar. Pratikte bu optimizasyonlar yarım parmaklıklar kullanılarak ortadan kaldırılır, ancak bu, gelecekte değişebilecek bir uygulama detayıdır.

Herhangi bir dilde, çok iş parçacıklı oldukları için uçucu ifadenin anlamını önemsiyor olmanız, belleği iş parçacıkları arasında paylaşmayı düşündüğünüzü gösterir. Bunu yapmamayı düşünün. Programınızın anlaşılmasını çok daha zor hale getirir ve ince, yeniden üretilmesi imkansız hatalar içermesi çok daha olasıdır.


19
"uçucu, C / C ++ 'da hemen hemen yararsızdır." Bir şey değil! Dünyanın çok kullanıcı modu-masaüstü-merkezli bir görünümüne sahipsiniz ... ancak çoğu C ve C ++ kodu, bellek eşlemeli G / Ç için uçucuya çok ihtiyaç duyulan gömülü sistemlerde çalışır.
Ben Voigt

12
Ve geçici erişimin korunmasının nedeni, sadece dışsal koşulların bellek konumlarını değiştirebilmesi değildir. Erişimin kendisi başka eylemleri tetikleyebilir. Örneğin, bir okumanın bir FIFO ilerletmesi veya bir kesme bayrağını temizlemesi çok yaygındır.
Ben Voigt

3
@BenVoigt: İş parçacığı sorunlarıyla etkili bir şekilde başa çıkmak için gereksiz bir amaç benim amaçladığım anlamdı.
Eric Lippert

4
@DavidSchwartz Standart açıkça bellek eşlemeli GÇ'nin nasıl çalıştığını garanti edemez. Ancak bellek eşlemeli GÇ, neden volatileC standardına dahil edildi. Yine de, standart bir "erişimde" gerçekte ne olduğu gibi şeyleri belirleyemediği için, "Uçucu nitelikli türe sahip bir nesneye erişimi oluşturan şey, uygulama tanımlıdır" der. Bugün çok fazla uygulama, IMHO'nun lafzına uygun olsa bile standardın ruhunu ihlal ettiği kullanışlı bir erişim tanımı sağlamamaktadır.
James Kanze

8
Bu düzenleme kesin bir gelişmedir, ancak açıklamanız hala "bellek dışsal olarak değişebilir" konusuna çok odaklanmış durumda. volatileanlambilim bundan daha güçlüdür, derleyicinin istenen her erişimi (1.9 / 8, 1.9 / 12) oluşturması gerekir, sadece dışsal değişikliklerin en sonunda tespit edileceğini garanti etmez (1.10 / 27). Bellek eşlemeli G / Ç dünyasında, bir bellek okuması, bir özellik alıcı gibi keyfi ilişkili mantığa sahip olabilir. Mülk alıcılarına yapılan çağrıları belirttiğiniz kurallara göre optimize edemezsiniz volatileve Standart buna izin vermez.
Ben Voigt

13

David'in gözden kaçırdığı şey, C ++ standardının yalnızca belirli durumlarda etkileşimde bulunan birkaç iş parçacığının davranışını belirlemesi ve diğer her şeyin tanımlanmamış davranışla sonuçlanmasıdır. Atomik değişkenler kullanmıyorsanız, en az bir yazma içeren bir yarış koşulu tanımsızdır.

Sonuç olarak, derleyici herhangi bir senkronizasyon talimatından vazgeçme hakkına sahiptir, çünkü CPU'nuz yalnızca eksik senkronizasyon nedeniyle tanımsız davranış sergileyen bir programdaki farkı fark edecektir.


5
Güzelce açıkladı, teşekkürler. Standart , programın tanımlanmamış bir davranışı olmadığı sürece uçucu maddelere erişim sırasını yalnızca gözlemlenebilir olarak tanımlar .
Jonathan Wakely

4
Programın bir veri yarışına sahip olması durumunda, standart, programın gözlemlenebilir davranışına ilişkin herhangi bir koşul getirmez. Derleyicinin, programda mevcut olan veri yarışlarını önlemek için geçici erişimlere engeller eklemesi beklenmez; bu, açık bariyerler veya atomik işlemler kullanarak programcının işidir.
Jonathan Wakely

Neden bunu gözden kaçırdığımı düşünüyorsun? Sence bu benim argümanımın hangi kısmını geçersiz kılıyor? Derleyicinin herhangi bir senkronizasyondan vazgeçme hakkına% 100 katılıyorum.
David Schwartz

2
Bu basitçe yanlıştır veya en azından esas olanı görmezden gelir. volatileiplerle ilgisi yoktur; asıl amacı bellek eşlemeli GÇ'yi desteklemekti. Ve en azından bazı işlemcilerde, bellek eşlemeli IO'yu desteklemek için sınırlamalar gerekir. (Derleyiciler bunu yapmaz, ancak bu farklı bir konu.)
James Kanze

@JamesKanze'nin iş parçacıklarıyla ilgisi volatilevardır: volatilederleyicinin erişilebileceğini bilmeden erişilebilen bellekle ilgilenir ve belirli CPU üzerindeki iş parçacıkları arasında paylaşılan verilerin birçok gerçek dünyadaki kullanımını kapsar.
wonderguy

12

Öncelikle, C ++ standartları, atomik olmayan okuma / yazma işlemlerini düzgün bir şekilde sipariş etmek için gereken bellek engellerini garanti etmez. uçucu değişkenlerin MMIO ile kullanılması, sinyal işleme vb. için tavsiye edilir. Çoğu uygulamada uçucu çoklu iş parçacığı için kullanışlı değildir ve genellikle tavsiye edilmez.

Uçucu erişimlerin uygulanmasıyla ilgili olarak, bu derleyicinin seçimidir.

Gcc davranışını açıklayan bu makale , geçici bir nesneyi geçici belleğe bir dizi yazma işlemi sipariş etmek için bellek engeli olarak kullanamayacağınızı göstermektedir.

İcc davranışı ile ilgili olarak, bu kaynağı aynı zamanda uçucunun bellek erişimlerinin sipariş edilmesini garanti etmediğini söyleyen buldum .

Microsoft VS2013 derleyicisinin farklı bir davranışı vardır. Bu dokümantasyon , volatile'nin Release / Acquire semantiğini nasıl zorladığını ve uçucu nesnelerin çok iş parçacıklı uygulamalardaki kilitlerde / yayınlarda kullanılmasını nasıl sağladığını açıklar.

Dikkate alınması gereken bir diğer husus, aynı derleyicinin farklı bir davranışa sahip olabileceğidir . hedeflenen donanım mimarisine bağlı olarak uçucu olabilir . MSVS 2013 derleyicisine ilişkin bu gönderi , ARM platformları için uçucu ile derlemenin özelliklerini açıkça belirtir.

Yani cevabım:

C ++ uçucu anahtar sözcüğü bellek çiti sağlar mı?

olacaktır: muhtemelen garanti ama bazı derleyiciler bunu yapabilir değil. Bunun olduğu gerçeğine güvenmemelisiniz.


2
Optimizasyonu engellemez, sadece derleyicinin yükleri ve depoları belirli kısıtlamaların ötesinde değiştirmesini önler.
Dietrich Epp

Ne söylediğiniz net değil. volatileDerleyicinin yükleri / depoları yeniden düzenlemesini engelleyen bazı belirtilmemiş derleyicilerde durumun böyle olduğunu mu söylüyorsunuz ? Yoksa C ++ standardının bunu gerektirdiğini mi söylüyorsunuz? Ve eğer ikincisi ise, benim asıl soruda alıntılanan aksine argümanıma cevap verebilir misiniz?
David Schwartz

@DavidSchwartz Standart, bir değer aracılığıyla erişimlerin (herhangi bir kaynaktan) yeniden sıralanmasını önler volatile. Ancak "erişim" tanımını uygulamaya kadar bıraktığı için, uygulama umursamıyorsa bu bize pek bir şey kazandırmaz.
James Kanze

Sanırım MSC derleyicilerinin bazı sürümleri için çit semantiği uyguladı volatile, ancak Visual Studios 2012'deki derleyiciden üretilen kodda hiçbir çit yok.
James Kanze

@JamesKanze Bu, temel olarak, tek taşınabilir davranışının volatile, standart tarafından özel olarak numaralandırılması olduğu anlamına gelir . ( setjmp, sinyaller vb.)
David Schwartz

7

Derleyici, bildiğim kadarıyla sadece Itanium mimarisine bir bellek çiti ekliyor.

volatileAnahtar kelime gerçekten iyi asenkron örneğin değişiklikleri, sinyal işleyici ve bellek eşlemeli kayıtları için kullanılır; çok iş parçacıklı programlama için genellikle yanlış bir araçtır.


1
Tür. 'derleyici' (msvc), ARM dışında bir mimari hedeflendiğinde ve / volatile: ms anahtarı kullanıldığında (varsayılan) bir bellek perdesi ekler. Bkz msdn.microsoft.com/en-us/library/12a04hfd.aspx . Diğer derleyiciler benim bilgime geçici değişkenler üzerine sınır koymazlar. Doğrudan donanım, sinyal işleyicileri veya c ++ 11 uyumlu olmayan derleyiciler ile ilgilenilmedikçe uçucu kullanımından kaçınılmalıdır.
Stefan

@Stefan No. volatile, donanımla hiç uğraşmayan birçok kullanım için son derece kullanışlıdır. Uygulamanın C / C ++ kodunu yakından takip eden CPU kodu oluşturmasını istediğinizde volatile.
wonderguy

7

"Derleyicinin" hangi derleyici olduğuna bağlıdır. Visual C ++ 2005'ten beri gerektiriyor. Ancak Standart bunu gerektirmediğinden diğer bazı derleyiciler gerektirmez.


VC ++ 2012 bir çit eklemek için görünmüyor: int volatile i; int main() { return i; }tam iki talimatları ile bir ana oluşturur: mov eax, i; ret 0;.
James Kanze

@JamesKanze: Tam olarak hangi sürüm? Ve varsayılan olmayan herhangi bir derleme seçeneği kullanıyor musunuz? Anlamsallık edinme ve yayınlama konusunda kesinlikle bahseden belgelere (ilk etkilenen sürüm) ve (en son sürüm) güveniyorum .
Ben Voigt

cl /help18.00.21005.1 sürümü diyor. İçinde bulunduğu dizin C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC. Komut penceresindeki başlıkta VS 2013 yazıyor. Yani sürümle ilgili olarak ... Kullandığım tek seçenek vardı /c /O2 /Fa. (Bu olmadan, /O2yerel yığın çerçevesini de kurar. Ancak hala çit talimatı yoktur.)
James Kanze

@JamesKanze: Mimariyle daha çok ilgilendim, örneğin "Microsoft (R) C / C ++ Optimizing Compiler Version 18.00.30723 for x64" Belki de engel yok çünkü x86 ve x64 bellek modellerinde oldukça güçlü önbellek tutarlılığı garantilerine sahip. ?
Ben Voigt

Olabilir. Ben gerçekten bilmiyorum Bunu içinde yaptığım gerçeği main, böylece derleyicinin tüm programı görebilmesi ve benimkinden önce başka hiçbir iş parçacığı olmadığını veya en azından değişkene başka hiçbir erişimin olmadığını (dolayısıyla önbellek sorunu olmayacağını) bilmesi, bunu etkileyebilir. yanı sıra, ama bir şekilde bundan şüpheliyim.
James Kanze

5

Bu, büyük ölçüde bellekten ve iş parçacığı olmadan ön-C ++ 11'e dayanmaktadır. Ancak komitede iş parçacığı ile ilgili tartışmalara katıldığım için, komitenin hiçbir zaman volatileileti dizileri arasında senkronizasyon için kullanılabilecek bir niyetinin olmadığını söyleyebilirim. Microsoft bunu önerdi, ancak teklif kabul edilmedi.

Anahtar özellikleri volatileuçucu erişim sadece IO gibi bir "gözlemlenebilir davranışı", temsil etmesidir. Aynı şekilde, derleyici belirli bir GÇ'yi yeniden sıralayamaz veya kaldıramaz, uçucu bir nesneye erişimi yeniden sıralayamaz veya kaldıramaz (veya daha doğrusu, uçucu nitelikli tipte bir lvalue ifadesi aracılığıyla erişir). Uçucunun asıl amacı aslında bellek eşlemeli GÇ'yi desteklemekti. Bununla birlikte, bunun "problemi", "geçici bir erişimi" oluşturan şeyin, tanımlanmış bir uygulama olmasıdır. Ve birçok derleyici, tanım "belleğe okuyan veya belleğe yazan bir talimat yürütülmüş gibi" uygular. Yararsız da olsa yasal bir tanım, eğer uygulama bunu belirtir. (Henüz herhangi bir derleyici için gerçek belirtimi bulamadım.

Muhtemelen (ve kabul ettiğim bir argüman), bu standardın amacını ihlal ediyor, çünkü donanım adresleri bellek eşlemeli GÇ olarak tanımadığı ve herhangi bir yeniden sıralamayı engellemediği sürece bellek eşlemeli GÇ için geçici bile kullanamazsınız, en azından Sparc veya Intel mimarilerinde. Ne kadar az, baktığım derleyicilerin hiçbiri (Sun CC, g ++ ve MSC) herhangi bir çit veya membran talimatını vermiyor. (Microsoft'un kuralları genişletmeyi önerdiği zaman hakkında, volatilesanırım bazı derleyiciler önerilerini uyguladılar ve geçici erişimler için çit talimatları yayınladılar. Son derleyicilerin ne yaptığını doğrulamadım, ancak bağlıysa beni şaşırtmazdı Bazı derleyici seçeneklerinde. Kontrol ettiğim sürüm - bunun VS6.0 olduğunu düşünüyorum - ancak çit yaymadı.)


Neden derleyicinin geçici nesnelere erişimi yeniden sıralayamayacağını veya kaldıramayacağını söylüyorsunuz ? Elbette, erişimler gözlemlenebilir bir davranışsa, o zaman kesinlikle CPU'yu önlemek, postalama arabellekleri, bellek denetleyicisi yazmak ve diğer her şeyi yeniden sıralamaktan kaçınmak kesinlikle eşit derecede önemlidir.
David Schwartz

@DavidSchwartz Çünkü standart öyle söylüyor. Elbette, pratik bir bakış açısına göre, doğruladığım derleyicilerin yaptıkları tamamen işe yaramaz, ancak standart çakal sözcükler bu kadar yeter ki uyumluluk iddia edebilirler (veya gerçekten belgelemişlerse yapabilir).
James Kanze

1
@DavidSchwartz: Çevre birimlere özel (veya muteks) bellek eşlemeli G / Ç için, volatileanlambilim mükemmel şekilde yeterlidir. Genellikle bu tür çevre birimleri bellek alanlarını önbelleğe alınamaz olarak bildirir ve bu da donanım düzeyinde yeniden sıralanmaya yardımcı olur.
Ben Voigt

@BenVoigt Bunu bir şekilde merak ettim: işlemcinin ilgilendiği adresin bellek eşlemeli GÇ olduğunu bir şekilde "bildiği" fikri. Bildiğim kadarıyla, Sparcs'ın bunun için herhangi bir desteği yok, bu yüzden bu, bir Sparc üzerinde Sun CC ve g ++ 'ı bellek eşlemeli GÇ için kullanılamaz hale getirebilir. (Buna baktığımda, esas olarak bir Sparc ile ilgilenmiştim.)
James Kanze

@JamesKanze: Yaptığım küçük aramaya göre, Sparc önbelleğe alınamayan bellek "alternatif görünümleri" için ayrılmış adres aralıklarına sahip gibi görünüyor. Uçucu erişiminiz ASI_REAL_IOadres alanının bir kısmına girdiği sürece , iyi olmanız gerektiğini düşünüyorum. (Altera NIOS, MMU baypasını kontrol eden adresin yüksek bitleri ile benzer bir teknik kullanır; eminim başkaları da vardır)
Ben Voigt

5

Mecbur değil. Uçucu bir senkronizasyon ilkel değildir. Sadece optimizasyonları devre dışı bırakır, yani bir iş parçacığı içinde soyut makinenin öngördüğü sırayla tahmin edilebilir bir okuma ve yazma sırası elde edersiniz. Ancak farklı konulardaki okuma ve yazma ilk etapta bir sıraya sahip değildir, sıralarını korumaktan veya korumamaktan bahsetmenin bir anlamı yoktur. Adalar arasındaki sıra senkronizasyon ilkelleri ile kurulabilir, onlar olmadan UB elde edersiniz.

Hafıza engelleriyle ilgili biraz açıklama. Tipik bir CPU'nun çeşitli seviyelerde bellek erişimi vardır. Bir hafıza hattı, birkaç seviye önbellek, ardından RAM vb.

Membar talimatları boru hattını yıkar. Okuma ve yazma işlemlerinin gerçekleştirilme sırasını değiştirmezler, sadece bekleyenleri belirli bir anda yürütülmeye zorlar. Çok iş parçacıklı programlar için kullanışlıdır, ancak başka türlü değildir.

Önbellekler normalde CPU'lar arasında otomatik olarak uyumludur. Önbelleğin RAM ile senkronize olduğundan emin olmak isterse, önbellek temizlemesi gerekir. Bir zardan çok farklı.


1
Yani C ++ standardının volatilederleyici optimizasyonlarını devre dışı bıraktığını mı söylüyorsunuz ? Bu hiç mantıklı değil. Derleyicinin yapabileceği herhangi bir optimizasyon, en azından prensipte, CPU tarafından eşit derecede iyi yapılabilir. Yani standart derleyici optimizasyonlarını devre dışı bıraktığını söyleseydi, bu taşınabilir kodda güvenebileceğiniz hiçbir davranış sağlamayacağı anlamına gelirdi. Ancak bu kesinlikle doğru değil çünkü taşınabilir kod, setjmpsinyallere göre davranışına güvenebilir .
David Schwartz

1
@DavidSchwartz Hayır, standart böyle bir şey söylemiyor. Optimizasyonları devre dışı bırakmak, standardı uygulamak için genellikle yapılan şeydir. Standart, gözlemlenebilir davranışın soyut makinenin gerektirdiği sırada olmasını gerektirir. Soyut makine herhangi bir sipariş gerektirmediğinde, uygulama herhangi bir sipariş kullanmakta veya hiç sipariş vermemek konusunda serbesttir. Ek senkronizasyon uygulanmadıkça, farklı iş parçacıklarındaki uçucu değişkenlere erişim sıralanmaz.
n. zamirler 'm.

1
@DavidSchwartz Kesin olmayan ifadeler için özür dilerim. Standart, optimizasyonların devre dışı bırakılmasını gerektirmez. Hiç optimizasyon kavramı yoktur. Bunun yerine, uygulamada derleyicilerin belirli optimizasyonları, gözlemlenebilir okuma ve yazma sırasının standarda uygun olacağı şekilde devre dışı bırakmasını gerektiren davranışı belirtir.
n. zamirler 'm.

1
Standart, uygulamaların "gözlemlenebilir okuma ve yazma sırasını" istedikleri gibi tanımlamasına izin verdiği için bunu gerektirmez. Uygulamalar, optimizasyonların devre dışı bırakılması gerektiği şekilde gözlemlenebilir dizileri tanımlamayı seçerse, o zaman yaparlar. Değilse, o zaman değil. Öngörülebilir bir okuma ve yazma dizisi elde edersiniz, ancak ve ancak uygulama bunu size vermeyi seçerse.
David Schwartz

1
Hayır, uygulamanın tek bir erişimi neyin oluşturduğunu tanımlaması gerekir. Bu tür erişimlerin sırası, soyut makine tarafından belirlenir. Bir uygulama düzeni korumalıdır. Standart, normatif olmayan bir kısımda da olsa, "uçucu, nesneyi içeren agresif optimizasyondan kaçınmak için uygulamaya yönelik bir ipucu" olduğunu açıkça belirtir, ancak amaç açıktır.
n. zamirler 'm.

4

Derleyicinin volatile, yalnızca ve ancak volatile, standart çalışmada ( setjmp, sinyal işleyicileri, vb.) Belirli platformda belirtilen kullanımları yapmak için gerekli olması durumunda, erişimlerin etrafına bir bellek çiti eklemesi gerekir .

Bazı derleyicilerin volatile, bu platformlarda daha güçlü veya kullanışlı hale getirmek için C ++ standardının gerektirdiğinin ötesine geçtiğini unutmayın . Taşınabilir kod güvenmemelidirvolatile , C ++ standardında belirtilenin ötesinde bir şey yapmaya dayanmamalıdır.


2

Kesinti hizmeti rutinlerinde her zaman uçucu kullanırım, örneğin ISR (genellikle montaj kodu) bazı bellek konumlarını değiştirir ve kesme bağlamının dışında çalışan daha yüksek düzeyli kod, geçici bir işaretçi aracılığıyla bellek konumuna erişir.

Bunu hem RAM hem de bellek eşlemeli GÇ için yapıyorum.

Buradaki tartışmaya dayanarak, bu hala geçerli bir uçucu kullanım gibi görünüyor, ancak birden fazla iş parçacığı veya CPU ile ilgisi yok. Bir mikro denetleyicinin derleyicisi başka erişimlerin olamayacağını "bilirse" (örneğin, her şey yonga üzerindedir, önbellek yoktur ve yalnızca bir çekirdek vardır), bir bellek çitinin hiç de ima edilmediğini düşünürdüm, derleyici sadece belirli optimizasyonları engellemesi gerekiyor.

Nesne kodunu çalıştıran "sisteme" daha fazla şey koyduğumuzda, neredeyse tüm bahisler kapalıdır, en azından bu tartışmayı böyle okudum. Bir derleyici nasıl tüm temelleri kapsayabilir?


0

Bence uçucu ve komut yeniden sıralama konusundaki kafa karışıklığı, CPU'ların yaptığı 2 yeniden sıralama kavramından kaynaklanıyor:

  1. Arıza yürütme.
  2. Diğer CPU'lar tarafından görüldüğü gibi bellek okuma / yazma dizisi (her CPU'nun farklı bir sıra görebileceği bir anlamda yeniden sıralama).

Uçucu, bir derleyicinin tek iş parçacıklı yürütmeyi varsayarak kodu nasıl oluşturduğunu etkiler (buna kesmeler dahildir). Bellek engeli talimatları hakkında hiçbir şey ifade etmez, ancak daha ziyade bir derleyicinin bellek erişimleriyle ilgili belirli türdeki optimizasyonları gerçekleştirmesini engeller.
Tipik bir örnek, bir kayıtta önbelleğe alınmış bir değer kullanmak yerine, bir değeri bellekten yeniden almaktır.

Arıza yürütme

Son sonucun orijinal kodda gerçekleşmiş olması koşuluyla, CPU'lar talimatları sıra dışı / spekülatif olarak yürütebilirler. Derleyiciler yalnızca her koşulda doğru olan dönüşümleri gerçekleştirebildiğinden CPU'lar, derleyicilerde izin verilmeyen dönüşümleri gerçekleştirebilir. Aksine, CPU'lar bu optimizasyonların geçerliliğini kontrol edebilir ve yanlış oldukları ortaya çıkarsa geri çekilebilir.

Diğer CPU'lar tarafından görülen bellek okuma / yazma sırası

Bir talimat dizisinin nihai sonucu olan etkin sıra, bir derleyici tarafından üretilen kodun anlambilimiyle uyumlu olmalıdır. Ancak, CPU tarafından seçilen gerçek yürütme sırası farklı olabilir. Diğer CPU'larda görüldüğü gibi etkili sıra (her CPU'nun farklı bir görünümü olabilir) bellek engelleri tarafından sınırlandırılabilir.
Ne kadar etkili ve gerçek düzenin farklı olabileceğinden emin değilim çünkü bellek engellerinin CPU'ların sıra dışı yürütmeyi gerçekleştirmesini ne ölçüde engelleyebileceğini bilmiyorum.

Kaynaklar:


0

Modern OpenGL ile çalışan 3D Grafik ve Oyun Motoru geliştirmesi için çevrimiçi indirilebilir bir video eğitimi üzerinde çalışırken. volatileSınıflarımızdan birinde kullandık . Öğretici web sitesi burada bulunabilir ve volatileanahtar kelime ile çalışan Shader Enginevideo 98 dizisinde bulunur. Bu çalışmalar bana ait değildir ancak Marek A. Krzeminski, MASconaylıdır ve bu video indirme sayfasından bir alıntıdır.

"Artık oyunlarımızın birden fazla iş parçacığında çalışmasını sağlayabildiğimiz için, iş parçacıkları arasında verileri düzgün bir şekilde senkronize etmek önemlidir. Bu videoda, volitile değişkenlerinin düzgün şekilde senkronize edilmesini sağlamak için bir volitile kilitleme sınıfının nasıl oluşturulacağını gösteriyorum ..."

Web sitesine abone olduysanız ve videolarına bu videoda erişiyorsanız , programlamayla kullanımıyla ilgili bu makaleye atıfta bulunur .Volatilemultithreading

İşte yukarıdaki bağlantıdan makale: http://www.drdobbs.com/cpp/volatile-the-multithreaded-programmers-b/184403766

uçucu: Çok İş Parçacıklı Programcının En İyi Arkadaşı

Andrei Alexandrescu, 01 Şubat 2001

Volatile anahtar sözcüğü, belirli eşzamansız olayların varlığında kodu yanlış hale getirebilecek derleyici optimizasyonlarını önlemek için tasarlanmıştır.

Ruh halinizi bozmak istemem ama bu sütun çok iş parçacıklı programlamanın korkunç konusuna değiniyor. Generic'in önceki bölümünde söylediği gibi, istisna korumalı programlama zorsa, çok iş parçacıklı programlamaya kıyasla çocuk oyuncağıdır.

Birden çok iş parçacığı kullanan programların genel olarak yazılması, doğruluğunun kanıtlanması, hata ayıklaması, bakımı ve evcilleştirilmesi zordur. Yanlış çok iş parçacıklı programlar, bir aksaklık olmadan yıllarca çalışabilir, yalnızca beklenmedik bir şekilde hata yapar çünkü bazı kritik zamanlama koşulları karşılandı.

Söylemeye gerek yok, çok iş parçacıklı kod yazan bir programcının alabileceği tüm yardıma ihtiyacı var. Bu sütun, çok iş parçacıklı programlarda ortak bir sorun kaynağı olan yarış koşullarına odaklanır ve bunlardan nasıl kaçınılacağına dair içgörüler ve araçlar sağlar ve şaşırtıcı bir şekilde, derleyicinin size bu konuda yardımcı olmak için çok çalışmasını sağlar.

Sadece Küçük Bir Anahtar Kelime

İş parçacıkları söz konusu olduğunda hem C hem de C ++ Standartları bariz bir şekilde sessiz olsalar da, uçucu anahtar kelime biçiminde çok iş parçacıklı okumaya biraz taviz veriyorlar.

Tıpkı daha iyi bilinen karşılığı const gibi, volatile bir tür değiştiricidir. Farklı iş parçacıklarında erişilen ve değiştirilen değişkenlerle birlikte kullanılması amaçlanmıştır. Temel olarak, uçucu olmadan, çok iş parçacıklı programlar yazmak imkansız hale gelir veya derleyici büyük optimizasyon fırsatlarını boşa harcar. Sırayla bir açıklama var.

Aşağıdaki kodu göz önünde bulundurun:

class Gadget {
public:
    void Wait() {
        while (!flag_) {
            Sleep(1000); // sleeps for 1000 milliseconds
        }
    }
    void Wakeup() {
        flag_ = true;
    }
    ...
private:
    bool flag_;
};

Yukarıdaki Gadget :: Wait işleminin amacı, flag_ üye değişkenini her saniye kontrol etmek ve bu değişken başka bir evre tarafından true olarak ayarlandığında geri dönmektir. En azından programcısının istediği buydu, ama ne yazık ki Bekle yanlış.

Derleyicinin Sleep (1000) 'in, flag_ üye değişkenini değiştiremeyecek bir harici kütüphaneye yapılan bir çağrı olduğunu anladığını varsayalım. Daha sonra derleyici flag_'i bir kayıtta önbelleğe alabileceği ve daha yavaş yerleşik belleğe erişmek yerine bu kaydı kullanabileceği sonucuna varır. Bu, tek iş parçacıklı kod için mükemmel bir optimizasyondur, ancak bu durumda, doğruluğa zarar verir: Bazı Gadget nesnelerini bekledikten sonra, başka bir iş parçacığı Uyanma çağırsa da, Bekle sonsuza kadar döngüye girer. Bunun nedeni, flag_ değişikliğinin flag_'i önbelleğe alan kayıtta yansıtılmamasıdır. Optimizasyon çok ... iyimser.

Kayıtlarda değişkenleri önbelleğe almak, çoğu zaman geçerli olan çok değerli bir optimizasyondur, bu yüzden onu boşa harcamak yazık olur. C ve C ++ size bu tür önbelleğe almayı açıkça devre dışı bırakma şansı verir. Bir değişken üzerinde uçucu değiştiriciyi kullanırsanız, derleyici bu değişkeni yazmaçlarda önbelleğe almaz - her erişim o değişkenin gerçek bellek konumuna ulaşır. Dolayısıyla, Gadget'ın Bekle / Uyandır kombinasyonunun çalışması için yapmanız gereken tek şey flag_'i uygun şekilde nitelendirmektir:

class Gadget {
public:
    ... as above ...
private:
    volatile bool flag_;
};

Uçucu maddenin mantığı ve kullanımının çoğu açıklaması burada durur ve birden çok iş parçacığında kullandığınız ilkel türleri geçici olarak nitelendirmenizi önerir. Bununla birlikte, uçucu ile yapabileceğiniz çok daha fazla şey var çünkü C ++ 'ın harika tip sisteminin bir parçası.

Kullanıcı Tanımlı Türlerle uçucu kullanma

Yalnızca ilkel türleri değil, aynı zamanda kullanıcı tanımlı türleri de geçici olarak niteleyebilirsiniz. Bu durumda volatile, türü const'a benzer şekilde değiştirir. (Aynı türe eşzamanlı olarak sabit ve uçucu da uygulayabilirsiniz.)

Sabit'ten farklı olarak, uçucu, ilkel türler ve kullanıcı tanımlı türler arasında ayrım yapar. Yani, sınıfların aksine, ilkel türler uçucu nitelikte olduklarında hala tüm işlemlerini (toplama, çarpma, atama vb.) Destekler. Örneğin, uçucu olmayan bir int atayabilirsiniz, ancak uçucu olmayan bir nesneyi uçucu bir nesneye atayamazsınız.

Kullanıcı tanımlı türlerde volatile'in nasıl çalıştığını bir örnek üzerinde gösterelim.

class Gadget {
public:
    void Foo() volatile;
    void Bar();
    ...
private:
    String name_;
    int state_;
};
...
Gadget regularGadget;
volatile Gadget volatileGadget;

Uçucu nesnelerin nesneler için o kadar yararlı olmadığını düşünüyorsanız, biraz sürpriz için hazırlanın.

volatileGadget.Foo(); // ok, volatile fun called for
                  // volatile object
regularGadget.Foo();  // ok, volatile fun called for
                  // non-volatile object
volatileGadget.Bar(); // error! Non-volatile function called for
                  // volatile object!

Nitelikli olmayan bir türden değişken muadiline dönüşüm önemsizdir. Ancak, const'ta olduğu gibi, geçici bir durumdan niteliksiz duruma geri dönemezsiniz. Bir alçı kullanmalısınız:

Gadget& ref = const_cast<Gadget&>(volatileGadget);
ref.Bar(); // ok

Uçucu nitelikte bir sınıf, arabiriminin yalnızca bir alt kümesine, sınıf uygulayıcısının denetimi altındaki bir alt kümeye erişim sağlar. Kullanıcılar bu türün arayüzüne yalnızca bir const_cast kullanarak tam erişim sağlayabilir. Ek olarak, tıpkı constness gibi, uçuculuk da sınıftan üyelerine yayılır (örneğin, volatileGadget.name_ ve volatileGadget.state_ uçucu değişkenlerdir).

uçucu, Kritik Bölümler ve Yarış Koşulları

Çok iş parçacıklı programlarda en basit ve en sık kullanılan senkronizasyon cihazı muteks'tir. Bir muteks, Acquire ve Release ilkellerini ortaya çıkarır. Bir iş parçacığında Acquire'ı çağırdığınızda, Acquire'ı çağıran diğer herhangi bir iş parçacığı engellenecektir. Daha sonra, iş parçacığı Release'i çağırdığında, bir Acquire çağrısında engellenen bir iş parçacığı serbest bırakılacaktır. Diğer bir deyişle, belirli bir muteks için, bir Acquire çağrısı ile bir Release çağrısı arasında yalnızca bir iş parçacığı işlemci süresini alabilir. Bir Acquire çağrısı ile bir Release çağrısı arasındaki yürütme koduna kritik bölüm adı verilir. (Windows terminolojisi biraz kafa karıştırıcıdır çünkü muteksin kendisini kritik bir bölüm olarak adlandırırken, "muteks" aslında süreçler arası bir mutekstir. Evre muteksi ve proses muteksi olarak adlandırılsalar güzel olurdu.)

Muteksler, verileri yarış koşullarına karşı korumak için kullanılır. Tanım gereği, daha fazla iş parçacığının veriler üzerindeki etkisi iş parçacığının nasıl programlandığına bağlı olduğunda bir yarış koşulu oluşur. Yarış koşulları, iki veya daha fazla iş parçacığı aynı verileri kullanmak için rekabet ettiğinde ortaya çıkar. İş parçacıkları zaman içinde rastgele anlarda birbirlerini kesebildikleri için veriler bozulabilir veya yanlış yorumlanabilir. Sonuç olarak, değişiklikler ve bazen verilere erişim kritik bölümlerle dikkatlice korunmalıdır. Nesne yönelimli programlamada, bu genellikle bir muteksi bir sınıfta üye değişken olarak depoladığınız ve o sınıfın durumuna her eriştiğinizde onu kullandığınız anlamına gelir.

Deneyimli çok iş parçacıklı programcılar yukarıdaki iki paragrafı okurken esnemiş olabilirler, ancak amaçları entelektüel bir çalışma sağlamaktır, çünkü şimdi uçucu bağlantıyla bağlantı kuracağız. Bunu, C ++ türlerinin dünyası ile iş parçacığı semantik dünyası arasında bir paralel çizerek yapıyoruz.

  • Kritik bir bölümün dışında, herhangi bir iş parçacığı herhangi bir zamanda herhangi bir diğerini kesebilir; kontrol yoktur, dolayısıyla birden çok iş parçacığından erişilebilen değişkenler uçucudur. Bu, geçici olayın orijinal amacına uygundur - derleyicinin, aynı anda birden çok iş parçacığı tarafından kullanılan değerleri istemeden önbelleğe almasını engellemek.
  • Bir muteks tarafından tanımlanan kritik bir bölümün içinde, yalnızca bir iş parçacığının erişimi vardır. Sonuç olarak, kritik bir bölümün içinde, yürütme kodunun tek iş parçacıklı semantiği vardır. Kontrollü değişken artık uçucu değildir - uçucu niteleyiciyi kaldırabilirsiniz.

Kısacası, iş parçacıkları arasında paylaşılan veriler kavramsal olarak kritik bir bölümün dışında uçucudur ve kritik bir bölümün içinde uçucu değildir.

Bir muteksi kilitleyerek kritik bir bölüme girersiniz. Bir türden geçici niteleyiciyi bir const_cast uygulayarak kaldırırsınız. Bu iki işlemi bir araya getirmeyi başarırsak, C ++ 'ın tip sistemi ile bir uygulamanın threading semantiği arasında bir bağlantı oluşturuyoruz. Derleyicinin bizim için yarış koşullarını kontrol etmesini sağlayabiliriz.

LockingPtr

Bir mutex edinimi ve bir const_cast toplayan bir araca ihtiyacımız var. Uçucu bir nesne objesi ve bir mutex mtx ile başlattığınız bir LockingPtr sınıfı şablonu geliştirelim. Kullanım ömrü boyunca, bir LockingPtr mtx'i elde etmeye devam eder. Ayrıca LockingPtr, uçucu-soyulmuş nesneye erişim sağlar. Erişim, operatör-> ve operatör * aracılığıyla akıllı bir işaretçi tarzında sunulur. Const_cast, LockingPtr içinde gerçekleştirilir. Çevirme anlamsal olarak geçerlidir çünkü LockingPtr, muteksi ömrü boyunca edinilmiş olarak tutar.

Öncelikle, LockingPtr'in çalışacağı bir Mutex sınıfının iskeletini tanımlayalım:

class Mutex {
public:
    void Acquire();
    void Release();
    ...    
};

LockingPtr kullanmak için, Mutex'i işletim sisteminizin yerel veri yapılarını ve ilkel işlevlerini kullanarak uygularsınız.

LockingPtr, kontrollü değişkenin türü ile şablonlanır. Örneğin, bir Widget'ı kontrol etmek istiyorsanız, değişken tipte uçucu Widget ile başlattığınız bir LockingPtr kullanırsınız.

LockingPtr'in tanımı çok basittir. LockingPtr karmaşık olmayan bir akıllı işaretçi uygular. Yalnızca bir sabit yayın ve kritik bir bölüm toplamaya odaklanır.

template <typename T>
class LockingPtr {
public:
    // Constructors/destructors
    LockingPtr(volatile T& obj, Mutex& mtx)
      : pObj_(const_cast<T*>(&obj)), pMtx_(&mtx) {    
        mtx.Lock();    
    }
    ~LockingPtr() {    
        pMtx_->Unlock();    
    }
    // Pointer behavior
    T& operator*() {    
        return *pObj_;    
    }
    T* operator->() {   
        return pObj_;   
    }
private:
    T* pObj_;
    Mutex* pMtx_;
    LockingPtr(const LockingPtr&);
    LockingPtr& operator=(const LockingPtr&);
};

Basitliğine rağmen, LockingPtr doğru çok iş parçacıklı kod yazmada çok yararlı bir yardımcıdır. Evreler arasında paylaşılan nesneleri uçucu olarak tanımlamalısınız ve bunlarla asla const_cast kullanmamalısınız - her zaman LockingPtr otomatik nesneleri kullanın. Bunu bir örnekle açıklayalım.

Bir vektör nesnesini paylaşan iki iş parçacığınızın olduğunu varsayalım:

class SyncBuf {
public:
    void Thread1();
    void Thread2();
private:
    typedef vector<char> BufT;
    volatile BufT buffer_;
    Mutex mtx_; // controls access to buffer_
};

Bir iş parçacığı işlevinin içinde, buffer_ üye değişkenine kontrollü erişim sağlamak için bir LockingPtr kullanmanız yeterlidir:

void SyncBuf::Thread1() {
    LockingPtr<BufT> lpBuf(buffer_, mtx_);
    BufT::iterator i = lpBuf->begin();
    for (; i != lpBuf->end(); ++i) {
        ... use *i ...
    }
}

Kodu yazmak ve anlamak çok kolaydır - ne zaman buffer_ kullanmanız gerekirse, ona işaret eden bir LockingPtr oluşturmanız gerekir. Bunu yaptıktan sonra, vektörün tüm arayüzüne erişebilirsiniz.

İşin güzel yanı, bir hata yaparsanız, derleyicinin bunu göstermesidir:

void SyncBuf::Thread2() {
    // Error! Cannot access 'begin' for a volatile object
    BufT::iterator i = buffer_.begin();
    // Error! Cannot access 'end' for a volatile object
    for ( ; i != lpBuf->end(); ++i ) {
        ... use *i ...
    }
}

Bir const_cast uygulayana veya LockingPtr kullanana kadar buffer_ işlevinin herhangi bir işlevine erişemezsiniz. Aradaki fark, LockingPtr'nin geçici değişkenlere const_cast uygulamak için sıralı bir yol sunmasıdır.

LockingPtr dikkat çekici şekilde ifade edicidir. Yalnızca bir işlevi çağırmanız gerekiyorsa, adsız bir geçici LockingPtr nesnesi oluşturabilir ve onu doğrudan kullanabilirsiniz:

unsigned int SyncBuf::Size() {
return LockingPtr<BufT>(buffer_, mtx_)->size();
}

İlkel Türlere Geri Dön

Nesneleri kontrolsüz erişime karşı ne kadar iyi uçucu koruduğunu ve LockingPtr'in iş parçacığı için güvenli kod yazmanın basit ve etkili bir yolunu nasıl sağladığını gördük. Şimdi uçucu tarafından farklı şekilde ele alınan ilkel türlere dönelim.

Birden çok iş parçacığının int türünde bir değişkeni paylaştığı bir örneği ele alalım.

class Counter {
public:
    ...
    void Increment() { ++ctr_; }
    void Decrement() { —ctr_; }
private:
    int ctr_;
};

Artış ve Azaltma farklı iş parçacıklarıyla çağrılacaksa, yukarıdaki parça buggy'dir. İlk olarak, ctr_ uçucu olmalıdır. İkinci olarak, ++ ctr_ gibi görünüşte atomik bir işlem bile aslında üç aşamalı bir işlemdir. Belleğin kendisinin aritmetik yeteneği yoktur. Bir değişkeni artırırken işlemci:

  • Bir kayıttaki değişkeni okur
  • Kayıttaki değeri artırır
  • Sonucu belleğe geri yazar

Bu üç aşamalı işleme RMW (Oku-Değiştir-Yaz) adı verilir. Bir RMW işleminin Değiştir bölümü sırasında, çoğu işlemci, diğer işlemcilerin belleğe erişmesine izin vermek için bellek veriyolunu boşaltır.

O sırada başka bir işlemci aynı değişken üzerinde bir RMW işlemi gerçekleştirirse, bir yarış durumumuz vardır: ikinci yazma, ilkinin etkisinin üzerine yazar.

Bundan kaçınmak için LockingPtr'a tekrar güvenebilirsiniz:

class Counter {
public:
    ...
    void Increment() { ++*LockingPtr<int>(ctr_, mtx_); }
    void Decrement() { —*LockingPtr<int>(ctr_, mtx_); }
private:
    volatile int ctr_;
    Mutex mtx_;
};

Şimdi kod doğrudur, ancak kalitesi SyncBuf'un koduyla karşılaştırıldığında daha düşüktür. Neden? Çünkü Counter ile, derleyici, yanlışlıkla ctr_'ye doğrudan erişirseniz (kilitlemeden) sizi uyarmaz. Derleyici, oluşturulan kod basitçe yanlış olsa da, ctr_ uçucu ise ++ ctr_ derler. Derleyici artık müttefikiniz değil ve yalnızca dikkatiniz yarış koşullarından kaçınmanıza yardımcı olabilir.

O halde ne yapmalısın? Basitçe, üst düzey yapılarda kullandığınız ilkel verileri kapsülleyin ve bu yapılarla uçucu kullanın. Paradoksal olarak, başlangıçta bu uçucu maddenin kullanım amacı olmasına rağmen, doğrudan yerleşik öğelerle uçucu kullanmak daha kötüdür!

uçucu Üye İşlevleri

Şimdiye kadar, değişken veri üyelerini bir araya getiren sınıflarımız oldu; şimdi daha büyük nesnelerin parçası olacak ve evreler arasında paylaşılan sınıflar tasarlamayı düşünelim. Uçucu üye işlevlerinin çok yardımcı olabileceği yer burasıdır.

Sınıfınızı tasarlarken, yalnızca iş parçacığı için güvenli olan üye işlevleri geçici olarak nitelendirirsiniz. Dışarıdan gelen kodun herhangi bir zamanda herhangi bir koddan geçici işlevleri çağıracağını varsaymalısınız. Unutmayın: uçucu, ücretsiz çok iş parçacıklı koda eşittir ve kritik bölüm yoktur; uçucu olmayan, tek iş parçacıklı senaryoya veya kritik bir bölümün içine eşittir.

Örneğin, iki varyantta bir işlem uygulayan bir sınıf Widget'ı tanımlarsınız - iş parçacığı güvenli ve hızlı, korumasız olan.

class Widget {
public:
    void Operation() volatile;
    void Operation();
    ...
private:
    Mutex mtx_;
};

Aşırı yükleme kullanımına dikkat edin. Artık Widget'ın kullanıcısı, uçucu nesneler için tek tip bir sözdizimi kullanarak İşlemi başlatabilir ve iş parçacığı güvenliği elde edebilir ya da normal nesneler için hız alabilir. Kullanıcı, paylaşılan Widget nesnelerini geçici olarak tanımlama konusunda dikkatli olmalıdır.

Uçucu bir üye işlevini uygularken, ilk işlem genellikle bunu bir LockingPtr ile kilitlemektir. Daha sonra uçucu olmayan kardeş kullanılarak çalışma yapılır:

void Widget::Operation() volatile {
    LockingPtr<Widget> lpThis(*this, mtx_);
    lpThis->Operation(); // invokes the non-volatile function
}

Özet

Çok iş parçacıklı programlar yazarken, kendi yararınıza uçucu kullanabilirsiniz. Aşağıdaki kurallara uymalısınız:

  • Paylaşılan tüm nesneleri uçucu olarak tanımlayın.
  • İlkel türlerle doğrudan uçucu kullanmayın.
  • Paylaşılan sınıfları tanımlarken, iş parçacığı güvenliğini ifade etmek için geçici üye işlevlerini kullanın.

Bunu yaparsanız ve basit genel bileşen LockingPtr kullanırsanız, iş parçacığı güvenli kod yazabilir ve yarış koşulları hakkında çok daha az endişe edebilirsiniz, çünkü derleyici sizin için endişelenecek ve hatalı olduğunuz noktaları özenle işaret edecektir.

Dahil olduğum birkaç proje, büyük bir etki için uçucu ve LockingPtr kullanıyor. Kod temiz ve anlaşılır. Birkaç kilitlenme anımsıyorum, ancak kilitlenmeleri yarış koşullarına tercih ederim çünkü hata ayıklaması çok daha kolay. Yarış koşullarıyla ilgili neredeyse hiçbir sorun yoktu. Ama asla bilemezsin.

Teşekkürler

Anlayışlı fikirlerle yardımcı olan James Kanze ve Sorin Jianu'ya çok teşekkürler.


Andrei Alexandrescu, Seattle, WA merkezli RealNetworks Inc.'de (www.realnetworks.com) Geliştirme Müdürü ve beğenilen Modern C ++ Tasarım kitabının yazarıdır. Kendisiyle www.moderncppdesign.com adresinden temasa geçilebilir. Andrei ayrıca C ++ Semineri'nin (www.gotw.ca/cpp_seminar) öne çıkan eğitmenlerinden biridir.

Bu makale biraz eski olabilir, ancak derleyicinin bizim için yarış koşullarını kontrol etmesini sağlarken olayları eşzamansız tutmaya yardımcı olmak için çok iş parçacıklı programlamanın kullanımında uçucu değiştiricinin mükemmel bir şekilde kullanılmasına yönelik iyi bir fikir veriyor. Bu, OP'nin bellek çiti oluşturma konusundaki orijinal sorusuna doğrudan cevap vermeyebilir, ancak bunu, çok iş parçacıklı uygulamalarla çalışırken uçucu maddenin iyi bir kullanımına yönelik mükemmel bir referans olarak başkaları için bir yanıt olarak göndermeyi seçiyorum.


0

Anahtar kelime, volatiletemelde, bir nesnenin okunup yazılmasının tam olarak program tarafından yazıldığı gibi gerçekleştirilmesi ve hiçbir şekilde optimize edilmemesi gerektiği anlamına gelir . İkili kod C veya C ++ kodunu takip etmelidir: bunun okunduğu bir yük, yazmanın olduğu bir depo.

Bu ayrıca, hiçbir okumanın tahmin edilebilir bir değerle sonuçlanmasının beklenmemesi gerektiği anlamına gelir: derleyici, aynı uçucu nesneye yazıldıktan hemen sonra bile bir okuma hakkında hiçbir şey varsaymamalıdır:

volatile int i;
i = 1;
int j = i; 
if (j == 1) // not assumed to be true

volatile"C yüksek seviyeli bir montaj dilidir" araç kutusundaki en önemli araç olabilir .

Eşzamansız değişikliklerle ilgilenen kod davranışını sağlamak için geçici bir nesne bildirmenin yeterli olup olmadığı platforma bağlıdır: farklı CPU, normal bellek okumaları ve yazmaları için farklı seviyelerde garantili senkronizasyon sağlar. Bu alanda uzman değilseniz, muhtemelen bu kadar düşük seviyeli çoklu okuma kodu yazmaya çalışmamalısınız.

Atomik ilkeller, kod hakkında akıl yürütmeyi kolaylaştıran çok iş parçacıklı nesnelerin güzel bir üst düzey görünümünü sağlar. Hemen hemen tüm programcılar, muteksler, okuma-yazma kilitleri, semaforlar veya diğer engelleme ilkelleri gibi karşılıklı dışlamalar sağlayan atomik ilkelleri veya ilkelleri kullanmalıdır.

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.