İlk olarak, bir Dil Avukatı gibi düşünmeyi öğrenmelisiniz.
C ++ belirtimi herhangi bir derleyici, işletim sistemi veya CPU ile ilgili değildir. Gerçek sistemlerin genelleştirilmesi olan soyut bir makineye atıfta bulunur . Dil Avukatı dünyasında, programcının işi soyut makine için kod yazmaktır; derleyicinin işi bu kodu bir beton makinesinde gerçekleştirmektir. Şartnameye katı bir şekilde kodlama yaparak, kodunuzun bugün veya 50 yıl sonra uyumlu bir C ++ derleyicisine sahip herhangi bir sistemde değişiklik yapmadan derlenip çalışacağından emin olabilirsiniz.
C ++ 98 / C ++ 03 spesifikasyonundaki soyut makine temelde tek iş parçacıklıdır. Bu nedenle, spesifikasyona göre "tamamen taşınabilir" olan çok iş parçacıklı C ++ kodunu yazmak mümkün değildir. Spesifikasyon, bellek yüklerinin ve depolarının atomisitesi veya yüklerin ve depoların meydana gelme sırası hakkında hiçbir şey söylemez , muteksler gibi şeylere aldırmayın.
Elbette, pthreads veya Windows gibi belirli beton sistemleri için pratikte çok iş parçacıklı kod yazabilirsiniz. Ancak C ++ 98 / C ++ 03 için çok iş parçacıklı kod yazmanın standart bir yolu yoktur .
C ++ 11'deki soyut makine tasarım açısından çok iş parçacıklı. Aynı zamanda iyi tanımlanmış bir bellek modeline sahiptir ; yani derleyicinin belleğe erişim söz konusu olduğunda neler yapabileceğini ve yapamayacağını söyler.
Bir çift global değişkenin iki iş parçacığıyla aynı anda erişildiği aşağıdaki örneği düşünün:
Global
int x, y;
Thread 1 Thread 2
x = 17; cout << y << " ";
y = 37; cout << x << endl;
Thread 2 çıktısı ne olabilir?
C ++ 98 / C ++ 03 altında, bu Tanımlanmamış Davranış bile değildir; sorunun kendisi anlamsızdır, çünkü standart "iş parçacığı" olarak adlandırılan herhangi bir şeyi düşünmez.
C ++ 11 altında, sonuç Tanımlanmamış Davranıştır, çünkü yüklerin ve depoların genel olarak atomik olması gerekmez. Bu bir gelişme gibi görünmeyebilir ... Ve kendi başına, öyle değil.
Ancak C ++ 11 ile şunu yazabilirsiniz:
Global
atomic<int> x, y;
Thread 1 Thread 2
x.store(17); cout << y.load() << " ";
y.store(37); cout << x.load() << endl;
Şimdi işler çok daha ilginç hale geliyor. Her şeyden önce, buradaki davranış tanımlanır . İş parçacığı 2 artık 0 0
(İş parçacığı 1'den önce çalışıyorsa), 37 17
(İş parçacığı 1'den sonra çalışıyorsa) veya 0 17
(İş parçacığı 1 x'e atadıktan sonra ancak y'ye atamadan önce çalışıyorsa ) yazdırabilir .
Ya baskı yapılamamasıdır 37 0
11 C ++ atomik yükler / mağazalar için varsayılan mod zorlamak için, çünkü sıralı tutarlılık . Bu, tüm yüklerin ve depoların her iş parçacığına yazdığınız sırayla "sanki" olması gerektiği anlamına gelirken, iş parçacıkları arasındaki işlemler sistemden istendiği gibi araya eklenebilir. Yani atomics varsayılan davranışı hem de sağlar atomicity ve sipariş yükler ve mağazalar için.
Şimdi, modern bir CPU'da, sıralı tutarlılığın sağlanması pahalı olabilir. Özellikle, derleyicinin buradaki her erişim arasında tam gelişmiş bellek engelleri yayması muhtemeldir. Ancak algoritmanız sıra dışı yükleri ve depoları tolere edebilirse; örneğin, atomisite gerektiriyor ancak sipariş vermiyorsa; yani, 37 0
bu programdan çıktı olarak tolere edilebilirse, bunu yazabilirsiniz:
Global
atomic<int> x, y;
Thread 1 Thread 2
x.store(17,memory_order_relaxed); cout << y.load(memory_order_relaxed) << " ";
y.store(37,memory_order_relaxed); cout << x.load(memory_order_relaxed) << endl;
CPU ne kadar modern olursa, önceki örnekten daha hızlı olma olasılığı o kadar yüksektir.
Son olarak, sadece belirli yükleri ve depoları düzenli tutmanız gerekiyorsa, şunları yazabilirsiniz:
Global
atomic<int> x, y;
Thread 1 Thread 2
x.store(17,memory_order_release); cout << y.load(memory_order_acquire) << " ";
y.store(37,memory_order_release); cout << x.load(memory_order_acquire) << endl;
Bu bizi sipariş edilen yüklere ve depolara geri götürüyor - bu yüzden 37 0
artık olası bir çıktı değil - ancak bunu minimum ek yük ile yapıyor. (Bu önemsiz örnekte, sonuç tam gelişmiş sıralı tutarlılıkla aynıdır; daha büyük bir programda böyle olmaz.)
Tabii ki, görmek istediğiniz tek çıktılar 0 0
ya da ise 37 17
, bir muteksi orijinal kodun etrafına sarabilirsiniz. Ama şimdiye kadar okuduysanız, bunun nasıl çalıştığını zaten biliyorsunuzdur ve bu cevap zaten istediğimden daha uzundur :-).
Yani, sonuçta. Muteksler harika ve C ++ 11 bunları standartlaştırıyor. Ancak bazen performans nedenlerinden dolayı daha düşük seviyeli ilkeller (örn. Klasik çift kontrollu kilitleme deseni ) istersiniz . Yeni standart, muteksler ve koşul değişkenleri gibi üst düzey gadget'lar sağlar ve ayrıca atomik tipler ve bellek bariyerinin çeşitli lezzetleri gibi düşük seviye gadget'lar sağlar. Artık tamamen standart tarafından belirtilen dilde karmaşık, yüksek performanslı eşzamanlı rutinler yazabilirsiniz ve kodunuzun hem günümüz sistemlerinde hem de yarınlarda derlenip değişmeyeceğinden emin olabilirsiniz.
Açıkçası, bir uzman değilseniz ve bazı ciddi düşük seviyeli kodlar üzerinde çalışmadığınız sürece, mutekslere ve koşul değişkenlerine bağlı kalmalısınız. Yapmayı planladığım şey bu.
Bu konu hakkında daha fazla bilgi için bu blog yayınına bakın .