Xbox 360 ve Microsoft Windows için Kilitsiz Programlama Konuları

Kilitsiz programlama, kilit alma ve bırakma maliyeti olmadan değişen verileri birden çok iş parçacığı arasında güvenli bir şekilde paylaşmanın bir yoludur. Bu bir panacea gibi görünür, ancak kilitsiz programlama karmaşık ve incedir ve bazen sunduğu avantajları sağlamaz. Xbox 360'ta kilitsiz programlama özellikle karmaşıktır.

Kilit gerektirmeyen programlama, çok iş parçacıklı programlama için geçerli bir tekniktir, ancak dikkatli kullanılmalıdır. Kullanmadan önce karmaşıklıkları anlamanız ve gerçekten beklediğiniz kazanımları sağladığından emin olmak için dikkatli bir şekilde ölçmeniz gerekir. Çoğu durumda, verileri daha az sık paylaşma gibi daha basit ve daha hızlı çözümler vardır ve bunun yerine kullanılması gerekir.

Kilitsiz programlamayı doğru ve güvenli bir şekilde kullanmak hem donanımınız hem de derleyiciniz hakkında önemli ölçüde bilgi gerektirir. Bu makalede, kilitsiz programlama tekniklerini kullanmaya çalışırken dikkat edilmesi gereken bazı sorunlara genel bir bakış verilmektedir.

Kilitlerle Programlama

Çok iş parçacıklı kod yazarken genellikle iş parçacıkları arasında veri paylaşmak gerekir. Paylaşılan veri yapılarını aynı anda okuyan ve yazan birden çok iş parçacığı varsa bellek bozulması oluşabilir. Bu sorunu çözmenin en basit yolu kilitleri kullanmaktır. Örneğin, ManipulateSharedData'nın aynı anda yalnızca bir iş parçacığı tarafından yürütülmesi gerekiyorsa, aşağıdaki kodda olduğu gibi bunu garanti etmek için bir CRITICAL_SECTION kullanılabilir:

// Initialize
CRITICAL_SECTION cs;
InitializeCriticalSection(&cs);

// Use
void ManipulateSharedData()
{
    EnterCriticalSection(&cs);
    // Manipulate stuff...
    LeaveCriticalSection(&cs);
}

// Destroy
DeleteCriticalSection(&cs);

Bu kod oldukça basit ve basittir ve doğru olduğunu söylemek kolaydır. Ancak kilitlerle programlamanın çeşitli olası dezavantajları vardır. Örneğin, iki iş parçacığı aynı iki kilidi edinmeye çalışır ancak bunları farklı bir sırayla edinirse, kilitlenme durumu ortaya çıkabilir. Bir program, kötü tasarım nedeniyle veya iş parçacığının daha yüksek öncelikli bir iş parçacığı tarafından yer değiştirilmesi nedeniyle çok uzun süre kilidi elinde tutuyorsa, diğer iş parçacıkları uzun süre engellenebilir. Yazılım iş parçacıklarına geliştirici tarafından bir donanım iş parçacığı atandığından ve işletim sistemi boşta olsa bile bunları başka bir donanım iş parçacığına taşımadığından bu risk özellikle Xbox 360'ta büyük bir risktir. Xbox 360 ayrıca, düşük öncelikli bir iş parçacığının kilidi serbest bırakmasını beklerken yüksek öncelikli bir iş parçacığının döngüde döndürdüğü öncelik ters çevirmeye karşı da korumaya sahip değildir. Son olarak, ertelenen yordam çağrısı veya kesme hizmeti yordamı bir kilit almaya çalışırsa kilitlenme yaşayabilirsiniz.

Bu sorunlara rağmen, eşzamanlama ilkelere, kritik bölümler gibi, genellikle birden çok iş parçacığını koordine etmenin en iyi yoludur. Eşitleme ilkelleri çok yavaşsa, en iyi çözüm genellikle bunları daha az sık kullanmaktır. Ancak, ekstra karmaşıklığı karşılayabilenler için bir diğer seçenek de kilitsiz programlamadır.

Kilitsiz Programlama

Adından da anlaşılacağı gibi, kilitsiz programlama, kilit kullanmadan paylaşılan verileri güvenli bir şekilde işlemeye yönelik bir teknik ailesidir. İletileri geçirmek, veri listelerini ve kuyruklarını paylaşmak ve diğer görevleri paylaşmak için kilitsiz algoritmalar vardır.

Kilitsiz programlama yaparken ilgilenmeniz gereken iki zorluk vardır: atomik olmayan işlemler ve yeniden sıralama.

Atomik Olmayan İşlemler

Atomik işlem bölünemez olan işlemdir; diğer iş parçacıklarının yarıya kadar işlem tamamlandığında işlemi asla görmemeleri garanti edilir. Atomik işlemler, kilitsiz programlama için önemlidir çünkü aksi takdirde diğer iş parçacıkları yarı yazılmış değerler veya başka bir şekilde tutarsız durumlar görebilir.

Tüm modern işlemcilerde, doğal olarak hizalanmış yerel türlerin okuma ve yazmalarının atomik olduğunu varsayabilirsiniz. Bellek veri yolu en az okunan veya yazılan tür kadar geniş olduğu sürece, CPU bu türleri tek bir veri yolu işleminde okur ve yazar ve diğer iş parçacıklarının bunları yarım tamamlanmış durumda görmesini imkansız hale getirir. x86 ve x64'te, sekiz bayttan büyük okuma ve yazmaların atomik olması garanti değildir. Bu, akış SIMD uzantısı (SSE) yazmaçlarının 16 baytlık okuma ve yazma işlemlerinin ve dize işlemlerinin atomik olmayabileceği anlamına gelir.

Doğal hizalı olmayan türlerin okuma ve yazma işlemleri, örneğin dört baytlık sınırları aşan DWORD yazımları, atomik olma garantisine sahip değildir. CPU'nun bu okuma ve yazma işlemlerini birden çok veri yolu işlemi olarak yapması gerekebilir ve bu da başka bir iş parçacığının okuma veya yazma işleminin ortasındaki verileri değiştirmesine veya görmesine izin verebilir.

Paylaşılan bir değişkeni artırdığınızda oluşan okuma-değiştirme-yazma dizisi gibi bileşik işlemler atomik değildir. Xbox 360'ta bu işlemler birden çok yönerge (lwz, addi ve stw) olarak uygulanır ve iş parçacığı işlem sırası sırasında değiştirilebilir. x86 ve x64'te, bellekteki bir değişkeni artırmak için kullanılabilecek tek bir yönerge (inc) vardır. Bu yönergeyi kullanırsanız, bir değişkeni artırmak tek işlemcili sistemlerde atomiktir, ancak yine de çok işlemcili sistemlerde atomik değildir. x86 ve x64 tabanlı çok işlemcili sistemlerde inc işleminin atomik olarak yapılabilmesi için, kilit ön eki kullanılmalıdır. Bu ön ek, diğer işlemcilerin, inc yönergesinin okuma ve yazma işlemleri arasında kendi okuma-değiştirme-yazma dizilerini yapmalarını engeller.

Aşağıdaki kodda bazı örnekler gösterilmektedir:

// This write is not atomic because it is not natively aligned.
DWORD* pData = (DWORD*)(pChar + 1);
*pData = 0;

// This is not atomic because it is three separate operations.
++g_globalCounter;

// This write is atomic.
g_alignedGlobal = 0;

// This read is atomic.
DWORD local = g_alignedGlobal;

Atomikliği Garanti Etme

Aşağıdakilerin bir bileşimiyle atomik işlemleri kullandığınızdan emin olabilirsiniz:

  • Doğal atomik işlemler
  • Bileşik işlemleri saran kilitler
  • Popüler bileşik işlemlerin atomik sürümlerini uygulayan işletim sistemi işlevleri

Değişkeni artırmak atomik bir işlem değildir ve birden çok iş parçacığında yürütülürse artırma veri bozulmasına neden olabilir.

// This will be atomic.
g_globalCounter = 0;

// This is not atomic and gives undefined behavior
// if executed on multiple threads
++g_globalCounter;

Win32, çeşitli yaygın işlemlerin atomik okuma-değiştirme-yazma sürümlerini sunan bir işlev ailesi ile birlikte gelir. Bunlar InterlockedXxx işlev ailesidir. Paylaşılan değişkenin tüm değişiklikleri bu işlevleri kullanırsa, değişiklikler iş parçacığı açısından güvenli olacaktır.

// Incrementing our variable in a safe lockless way.
InterlockedIncrement(&g_globalCounter);

Yeniden sıralama

Daha ince bir sorun yeniden sıralamadır. Okuma ve yazma işlemleri her zaman kodunuzda yazdığınız sırada gerçekleşmez ve bu durum kafa karıştırıcı sorunlara yol açabilir. Birçok çok iş parçacıklı algoritmada, bir iş parçacığı bazı veriler yazar ve ardından verilerin hazır olduğunu diğer iş parçacıklarına bildiren bir işaret yazar. Bu, yazma sürümü olarak bilinir. Yazma işlemleri yeniden sıralanırsa, diğer iş parçacıkları bayrağın ayarlandığını yazılan verilerden önce görebilir.

Benzer şekilde, çoğu durumda, bir iş parçacığı bir bayrağı okur ve eğer bu bayrak, iş parçacığının paylaşılan verilere erişim sağladığını belirtiyorsa, bazı paylaşılan verileri de okur. Bu, okuma-yakalama olarak bilinir. Okumalar yeniden sıralanırsa, veriler bayraktan önce paylaşılan depolamadan okunabilir ve görülen değerler güncel olmayabilir.

Okumaların ve yazmaların yeniden sıralanması hem derleyici hem de işlemci tarafından yapılabilir. Derleyiciler ve işlemciler bu yeniden sıralamayı yıllardır yapmıştır, ancak tek işlemcili makinelerde sorun daha azdı. Bunun nedeni, tek işlemcili makinelerde (cihaz sürücüsünün bir parçası olmayan cihaz dışı sürücü kodu için) okuma ve yazmaların CPU tarafından yeniden düzenlenmesinin görünmez olması ve derleyicinin okuma ve yazmaları yeniden düzenlemesinin tek işlemcili makinelerde sorun yaratma olasılığının daha düşük olmasıdır.

Derleyici veya CPU aşağıdaki kodda gösterilen yazmaları yeniden düzenlerse, başka bir iş parçacığı muhtemelen canlı bayrağının ayarlanmış olduğunu görebilir fakat x veya y için eski değerleri görmeye devam edebilir. Okuma sırasında da benzer yeniden düzenleme yapılabilir.

Bu kodda, bir iş parçacığı sprite dizisine yeni bir giriş ekler:

// Create a new sprite by writing its position into an empty
// entry and then setting the 'alive' flag. If 'alive' is
// written before x or y then errors may occur.
g_sprites[nextSprite].x = x;
g_sprites[nextSprite].y = y;
g_sprites[nextSprite].alive = true;

Bu sonraki kod bloğunda, başka bir iş parçacığı sprite dizisinden okur:

// Draw all sprites. If the reads of x and y are moved ahead of
// the read of 'alive' then errors may occur.
for( int i = 0; i < numSprites; ++i )
{
    if( g_sprites[nextSprite].alive )
    {
        DrawSprite( g_sprites[nextSprite].x,
                g_sprites[nextSprite].y );
    }
}

Bu sprite sistemini güvenli hale getirmek için hem derleyicinin hem de CPU'nun okuma ve yazmaları yeniden sıralamasını engellememiz gerekir.

CPU'nun Yazma İşlemlerini Yeniden Düzenlemesini Anlama

Bazı CPU'lar, yazmaları program dışı sırada diğer işlemciler veya cihazlara dışarıdan görünür olacak şekilde yeniden düzenler. Bu yeniden düzenleme, sürücü içermeyen tek iş parçacıklı kodda asla görünmez, ancak çok iş parçacıklı kodda sorunlara neden olabilir.

Xbox 360

Xbox 360 CPU yönergeleri yeniden sıralamasa da, yönergelerden sonra tamamlanan yazma işlemlerini yeniden düzenler. Yazmaların bu yeniden düzenlenmesine PowerPC bellek modeli tarafından özel olarak izin verilir.

Xbox 360'ta yazma işlemleri doğrudan L2 önbelleğine gitmez. Bunun yerine, L2 önbelleği yazma bant genişliğini geliştirmek için depolama kuyruklarından geçerler ve ardından depolama-toplama arabelleklerine giderler. Depolama toplama arabellekleri, tek işlemde 64 baytlık blokların L2 önbelleğine yazılmasına olanak tanır. Bellekte birkaç farklı alana verimli yazma olanağı sağlayan sekiz depo toplama arabelleği vardır.

Depolama toplama tamponları normalde ilk giren ilk çıkar (FIFO) sırasına göre L2 önbelleğine yazılır. Ancak, bir yazma işleminin hedef önbellek satırı L2 önbelleğinde değilse, önbellek satırı bellekten getirilirken bu yazma işlemi gecikebilir.

Depo toplama arabellekleri L2 önbelleğine katı FIFO sırasına göre yazıldığında bile, tek tek yazmaların L2 önbelleğine sırayla yazıldığını garanti etmez. Örneğin, CPU'0x1000 konuma, ardından 0x2000 konuma ve ardından 0x1004 konuma yazdığını düşünün. İlk yazma işlemi bir depo toplama arabelleği ayırır ve bu arabelleği kuyruğun önüne yerleştirir. İkinci yazma işlemi başka bir store-gather arabelleği ayırır ve bunu kuyruğun sonraki yerine koyar. Üçüncü yazma işlemi, verilerini kuyruğun önünde kalan ilk depo toplama arabelleğine ekler. Bu nedenle, üçüncü yazma işlemi ikinci yazmadan önce L2 önbelleğine gidiyor olur.

Depo toplama arabelleklerinin neden olduğu yeniden sıralama temel olarak tahmin edilemez, çünkü özellikle çekirdek üzerindeki her iki iş parçacığı da depo toplama arabelleklerini paylaşır ve bu da depo-toplama arabelleklerinin ayırma ve boşaltmasını yüksek oranda değişken hale getirir.

Bu, yazmaların nasıl yeniden sıralanabileceğini gösteren bir örnektir. Başka olasılıklar da olabilir.

x86 ve x64

x86 ve x64 CPU'ları yönergeleri yeniden sıralasa da, genellikle diğer yazma işlemlerine göre yazma işlemlerini yeniden sıralamaz. Yazma-birleştirme belleği için bazı istisnalar vardır. Ayrıca, dize işlemleri (MOVS ve STOS) ve 16 baytlık SSE yazma işlemleri dahili olarak yeniden sıralanabilir, ancak aksi takdirde yazma işlemleri birbirine göre yeniden sıralanmaz.

Okumaların CPU Tarafından Düzenlenmesini Anlama

Bazı CPU'lar, program dışı sırayla paylaşılan depolamadan etkili bir şekilde gelmeleri için okumaları yeniden düzenler. Bu yeniden düzenleme hiçbir zaman tek iş parçacıklı sürücü kodu içermeyen koda görünmez, ancak çok iş parçacıklı kodda sorunlara neden olabilir.

Xbox 360

Önbellek eksikleri, bazı okumaların geciktirilmesine neden olabilir ve bu da okumaların paylaşılan bellekten sıra dışı çıkmasına neden olur ve bu önbellek eksiklerinin zamanlaması temel olarak öngörülemez. Ön bellek yükleme ve dal tahmini de verilerin paylaşılan bellekten sıra dışı gelmesine yol açabilir. Bunlar, okumaların nasıl yeniden sıralanabileceğini gösteren birkaç örnektir. Başka olasılıklar da olabilir. Okumaların bu yeniden düzenlenmesine özellikle PowerPC bellek modeli tarafından izin verilir.

x86 ve x64

x86 ve x64 CPU'ları yönergeleri yeniden sıralasa da, genellikle okuma işlemlerini diğer okumalara göre yeniden sıralamaz. Dize işlemleri (MOVS ve STOS) ve 16 baytlık SSE okumaları dahili olarak yeniden sıralanabilir, ancak aksi takdirde okumalar birbirine göre yeniden sıralanmaz.

Diğer Yeniden Sıralama

x86 ve x64 CPU'ları yazmaları diğer yazmalara göre veya okumaları diğer okumalara göre yeniden sıralamaz, ancak okumaları yazmalara göre yeniden sıralayabilir. Özellikle, bir program bir konuma yazıyor ve ardından farklı bir konumdan okuyorsa, okunan veriler, yazılan veriler henüz oraya ulaşmadan önce paylaşılan bellekten gelebilir. Bu yeniden sıralama, Dekker'ın karşılıklı dışlama algoritmaları gibi bazı algoritmaları bozabilir. Dekker algoritmasında, her iş parçacığı kritik bölgeye girmek istediğini belirten bir bayrak ayarlar ve ardından diğer iş parçacığının kritik bölgede olup olmadığını veya bu bölgeye girmeye çalıştığını görmek için diğer iş parçacığının bayrağını denetler. İlk kod aşağıdaki gibidir.

volatile bool f0 = false;
volatile bool f1 = false;

void P0Acquire()
{
    // Indicate intention to enter critical region
    f0 = true;
    // Check for other thread in or entering critical region
    while (f1)
    {
        // Handle contention.
    }
    // critical region
    ...
}


void P1Acquire()
{
    // Indicate intention to enter critical region
    f1 = true;
    // Check for other thread in or entering critical region
    while (f0)
    {
        // Handle contention.
    }
    // critical region
    ...
}

Sorun, P0Acquire'deki f1 okumasının, f0'a yazma işlemi paylaşılan depolamaya geçmeden önce paylaşılan depolamadan okuyabilmesidir. Bu arada, P1Acquire'deki f0 okuması, f1'e yazma işlemi paylaşılan depolamaya geçmeden önce paylaşılan depolamadan okuyabilir. Bunun net etkisi, her iki iş parçacığının da bayraklarını TRUE olarak ayarlaması ve her iki iş parçacığının da diğer iş parçacığının bayrağını YANLIŞ olarak görmesidir, böylece her ikisi de kritik bölgeye girer. Bu nedenle, x86 ve x64 tabanlı sistemlerde yeniden sıralamayla ilgili sorunlar Xbox 360'tan daha az yaygın olsa da, kesinlikle yine de oluşabilir. Dekker algoritması, bu platformların hiçbirinde donanım bellek engelleri olmadan çalışmaz.

x86 ve x64 CPU'ları önceki okumadan önce bir yazmayı yeniden sıralamaz. x86 ve x64 CPU'lar yalnızca farklı konumları hedeflediklerinde okumaları önceki yazmalardan önce yeniden sıralar.

PowerPC CPU'ları, farklı adreslerde oldukları sürece, okumaları yazmalardan önce ve yazmaları okumalar öncesinde yeniden sıralayabilir.

Sıralama Özeti

Xbox 360 CPU, aşağıdaki tabloda gösterildiği gibi bellek işlemlerini x86 ve x64 CPU'larından çok daha agresif bir şekilde yeniden sıralar. Daha fazla ayrıntı için işlemci belgelerine bakın.

Etkinliği Yeniden Sıralama x86 ve x64 Xbox 360
Okumaların önünde ilerleyen okumalar Hayır Evet
Yazma işlemlerinin öncelikli olması Hayır Evet
Yazma işlemleri, okuma işlemlerinden önce gelir Hayır Evet
Yazma işlemlerinden önce gelen okumalar Evet Evet

 

Read-Acquire ve Write-Release Engelleri

Okuma ve yazmaların yeniden sıralanmasını önlemek için kullanılan ana yapılar, okuma-alma ve yazma-yayın bariyerleri olarak adlandırılır. Okuma alma, bir kaynağın sahipliğini elde etmek için bayrağın veya başka bir değişkenin okunmasının yanında yeniden sıralamaya karşı bir engeldir. Benzer şekilde, yazma serbest bırakması, bir kaynağın sahipliğini devretmek için bir bayrağın veya başka bir değişkenin yazılmasıdır ve yeniden sıralamaya karşı bir engel içerir.

Herb Sutter'ın resmi tanımları şunlardır:

  • Okuma alma işlemi, program sırasına göre onu izleyen aynı iş parçacığı tarafından tüm okuma ve yazma işlemleri öncesinde yürütülür.
  • Tüm okuma ve yazma işlemleri program sırasına göre kendisinden önce gelen aynı iş parçacığı tarafından yazıldıktan sonra bir yazma sürümü yürütülür.

Kodunuz bir kilidi elde ederek veya kilit olmadan paylaşılan bağlantılı listeden bir öğe çekerek bir belleğin sahipliğini kazandığında, her zaman bir okuma işlemi gerçekleşir—belleğin sahipliğinin ele geçirilip geçirilmediğini görmek için bir bayrağı veya işaretçiyi test edersiniz. Bu okuma, bir InterlockedXxx işleminin parçası olabilir; bu durumda hem okuma hem de yazma işlemi içerir, ancak sahipliğin kazanılıp kazanılmadığını gösteren okuma işlemidir. Belleğin sahipliği alındıktan sonra, değerler genellikle bu bellekten okunur veya yazılır ve bu okuma ve yazmaların sahiplik alındıktan sonra yürütülmesi çok önemlidir. Okuma-alma engeli bunu garanti eder.

Bir kilidi serbest bırakarak veya paylaşılan bağlantılı listeye bir öğe ekleyerek bazı belleğin sahipliği serbest bırakıldığında, her zaman diğer iş parçacıklarına belleğin artık kullanılabilir olduğunu bildiren bir yazma işlemi gerçekleşir. Kodunuz belleğin sahibi olduğu sürede, muhtemelen bellekten okudu veya yazdı ve sahipliği bırakmadan önce bu okuma ve yazmaların gerçekleşmesi çok önemlidir. Yazma bırakma engeli bunu garanti eder.

Okuma-alma ve yazma-yayınlama engellerini tek bir işlem olarak düşünmek en basit işlemdir. Ancak, bazen iki bölümden oluşturulmalıdır: bir okuma veya yazma ve bir engelin üzerinden okuma veya yazmanın geçmesine izin vermeyen bir engel. Bu durumda bariyerin yerleşimi kritik önem taşır. Okuma-alma engeli için önce bayrağın okunması, ardından engel ve ardından paylaşılan verilerin okuma ve yazma işlemleri gerçekleşir. Bir yazma-yayın engeli durumunda, paylaşılan verilerin okuma ve yazma işlemleri önce gelir, ardından engel gelir ve son olarak bayrak yazılır.

// Read that acquires the data.
if( g_flag )
{
    // Guarantee that the read of the flag executes before
    // all reads and writes that follow in program order.
    BarrierOfSomeSort();

    // Now we can read and write the shared data.
    int localVariable = sharedData.y;
    sharedData.x = 0;

    // Guarantee that the write to the flag executes after all
    // reads and writes that precede it in program order.
    BarrierOfSomeSort();
    
    // Write that releases the data.
    g_flag = false;
}

Okuma-edinim ile yazma-bırakma arasındaki tek fark, bellek engelinin konumudur. Okuma alma işlemi kilit işleminden sonra engele sahiptir ve yazma sürümü daha önce engele sahiptir. Her iki durumda da engel, kilitli belleğe yapılan başvurular ile kilit başvuruları arasında yer alır.

Hem veri alırken hem de veri yayınlarken neden engellere ihtiyaç duyulduğunu anlamak için, bu engelleri diğer işlemcilerle değil paylaşılan bellekle eşitlemeyi garanti etmek olarak düşünmek en iyisidir (ve en doğru). Bir işlemci paylaşılan belleğe veri yapısı yayınlamak için bir yazma sürümü kullanırsa ve başka bir işlemci paylaşılan bellekten bu veri yapısına erişim elde etmek için okuma-alma kullanırsa kod düzgün çalışır. İşlemcilerden biri uygun engeli kullanmıyorsa veri paylaşımı başarısız olabilir.

Platformunuz için derleyici ve CPU yeniden sıralamasını önlemek için doğru engeli kullanmak kritik önem taşır.

İşletim sistemi tarafından sağlanan eşitleme temel öğelerini kullanmanın avantajlarından biri, bunların tümünün uygun bellek engellerini içermesidir.

Derleyici Yeniden Sıralamasını Engelleme

Derleyicinin görevi, performansı geliştirmek için kodunuzu agresif bir şekilde iyileştirmektir. Bu, yararlı olduğu ve davranışı değiştirmeyecekleri her yerde yönergeleri yeniden düzenlemeyi içerir. C++ Standard çok iş parçacıklı işlemden hiç bahsetmediğinden ve derleyici hangi kodun iş parçacığı açısından güvenli olması gerektiğini bilmediğinden, derleyici, hangi yeniden düzenlemelerin güvenli bir şekilde gerçekleştirilebileceğine karar verirken kodunuzun tek iş parçacıklı olduğunu varsayar. Bu nedenle, okumaları ve yazmaları yeniden sıralamasına izin verilmediğinde derleyiciye bunu söylemeniz gerekir.

Visual C++ ile derleyici iç _ReadWriteBarrierkullanarak derleyicinin yeniden sıralanmasını engelleyebilirsiniz. Kodunuza _ReadWriteBarrier eklediğinizde, derleyici okuma ve yazma işlemlerini bu bariyer boyunca taşımaz.

#if _MSC_VER < 1400
    // With VC++ 2003 you need to declare _ReadWriteBarrier
    extern "C" void _ReadWriteBarrier();
#else
    // With VC++ 2005 you can get the declaration from intrin.h
#include <intrin.h>
#endif
// Tell the compiler that this is an intrinsic, not a function.
#pragma intrinsic(_ReadWriteBarrier)

// Create a new sprite by filling in a previously empty entry.
g_sprites[nextSprite].x = x;
g_sprites[nextSprite].y = y;
// Write-release, barrier followed by write.
// Guarantee that the compiler leaves the write to the flag
// after all reads and writes that precede it in program order.
_ReadWriteBarrier();
g_sprites[nextSprite].alive = true;

Aşağıdaki kodda, başka bir iş parçacığı sprite dizisinden okur:

// Draw all sprites.
for( int i = 0; i < numSprites; ++i )
{

    // Read-acquire, read followed by barrier.
    if( g_sprites[nextSprite].alive )
    {
    
        // Guarantee that the compiler leaves the read of the flag
        // before all reads and writes that follow in program order.
        _ReadWriteBarrier();
        DrawSprite( g_sprites[nextSprite].x,
                g_sprites[nextSprite].y );
    }
}

_ReadWriteBarrier ek yönergeler eklemediğini ve CPU'ların okuma ve yazmaları yeniden düzenlemesini engellemediğini, yalnızca derleyicinin bunları yeniden düzenlemesini engellediğini anlamak önemlidir. Bu nedenle _ReadWriteBarrier, x86 ve x64 üzerinde bir yazma yayın engeli uyguladığınızda yeterlidir (çünkü x86 ve x64 yazmaları yeniden sıralamaz ve bir kilidi serbest bırakmak için normal bir yazma yeterlidir), ancak diğer çoğu durumda CPU'un okumaları ve yazmaları yeniden sıralamasını önlemek de gerekir.

Önbelleğe alınamayan birleştirilmiş yazma belleğine yazarken yazma işlemlerinin sırasının değişmesini önlemek için _ReadWriteBarrier de kullanabilirsiniz. Bu durumda _ReadWriteBarrier, yazma işleminin işlemcinin tercih ettiği doğrusal sırada gerçekleşmesini garanti ederek performansın geliştirilmesine yardımcı olur.

Derleyici yeniden sıralamasını daha hassas bir şekilde denetlemek için _ReadBarrier ve _WriteBarrier içlerini kullanmak da mümkündür. Derleyici okumaları bir _ReadBarrierarasında taşımaz ve yazmaları _WriteBarrierboyunca taşımaz.

CPU Yeniden Sıralamasını Önleme

CPU yeniden sıralama, derleyici yeniden sıralamasından daha incedir. Bunun doğrudan gerçekleştiğini asla göremezsiniz, yalnızca açıklanamayan hatalar görürsünüz. Okuma ve yazma işlemleri için CPU'nun yeniden sıralanmasını önlemek için bazı işlemcilerde bellek engeli yönergelerini kullanmanız gerekir. Xbox 360 ve Windows'ta bir bellek engeli talimatının tüm amaçlı adı MemoryBarrier. Bu makro her platform için uygun şekilde uygulanır.

Xbox 360'ta, MemoryBarrierlwsync (basit eşitleme) olarak tanımlanır ve ppcintrinsics.h içinde tanımlanan __lwsync iç üzerinden de kullanılabilir. __lwsync ayrıca derleyici bellek engeli görevi de görür ve derleyici tarafından okuma ve yazmaların yeniden düzenlenmesini önler.

lwsync yönergesi, Xbox 360'ta bir işlemci çekirdeğini L2 önbelleğiyle eşitleyen bir bellek engelidir. lwsync'dan önce yapılan tüm yazmaların, sonrakilerden önce L2 önbelleğine ulaştığını garanti eder. Ayrıca, lwsync'i izleyen okumaların, önceki okumalardan daha eski verileri L2'den almasını engeller ve bu durumu garanti eder. Engellemediği bir yeniden sıralama türü, farklı bir adrese yazma işleminin önüne geçmektir. Bu nedenle lwsync, x86 ve x64 işlemcilerde varsayılan bellek sıralamasıyla eşleşen bellek sıralamasını zorlar. Tam bellek sıralaması sağlamak için daha pahalı senkronizasyon talimatı (ağır senkronizasyon olarak da bilinir) gerekir, ancak çoğu durumda bu gerekli değildir. Xbox 360'taki bellek yeniden sıralama seçenekleri aşağıdaki tabloda gösterilmiştir.

Xbox 360 Yeniden Sıralama Eşitleme yok lwsync Eşitleme
Okumaların öncelikli olarak ilerlemesi Evet Hayır Hayır
Yazma işlemleri öncesinden devam eden yazma işlemleri Evet Hayır Hayır
Yazma işlemleri, okuma işlemlerinin önüne geçer. Evet Hayır Hayır
Yazmalardan önce yapılan okumalar Evet Evet Hayır

 

PowerPC ayrıca isync ve eieio eşitleme talimatlarına da sahiptir (önbelleğe alma engellenmiş belleğe yeniden sıralamayı denetlemek için kullanılır). Bu eşitleme yönergeleri normal eşitleme amacıyla gerekli olmamalıdır.

Windows'da MemoryBarrier Winnt.h'de tanımlanır ve x86 veya x64 için derleme yaptığınıza bağlı olarak size farklı bir bellek engeli yönergesi verir. Bellek bariyeri yönergesi tam bir engel görevi görür ve bu da engelin üzerinde okuma ve yazmaların yeniden sıralanmasını önler. Bu nedenle Windows'da MemoryBarrier, Xbox 360'a kıyasla daha güçlü bir yeniden sıralama garantisi verir.

Xbox 360'ta ve diğer birçok CPU'da, CPU tarafından okuma yeniden sıralamanın engellenebileceği ek bir yol vardır. bir işaretçiyi okur ve sonra diğer verileri yüklemek için bu işaretçiyi kullanırsanız, CPU işaretçinin okumasının işaretçinin okunmasından daha eski olmadığını garanti eder. Kilit bayrağınız bir işaretçiyse ve paylaşılan verilerin tüm okumaları bu işaretçi üzerinden yapılıyorsa, belirli bir performans tasarrufu için MemoryBarrieratlanabilir.

Data* localPointer = g_sharedPointer;
if( localPointer )
{
    // No import barrier is needed--all reads off of localPointer
    // are guaranteed to not be reordered past the read of
    // localPointer.
    int localVariable = localPointer->y;
    // A memory barrier is needed to stop the read of g_global
    // from being speculatively moved ahead of the read of
    // g_sharedPointer.
    int localVariable2 = g_global;
}

MemoryBarrier yönergesi yalnızca önbelleğe alınabilen belleğe okuma ve yazmaların yeniden sıralanmasını engeller. Belleği PAGE_NOCACHE veya PAGE_WRITECOMBINE olarak ayırırsanız, cihaz sürücüsü yazarları ve Xbox 360'ta oyun geliştiricileri için yaygın bir teknik MemoryBarrier bu belleğe erişimleri etkilemez. Çoğu geliştirici, önbelleğe alınamayan belleği senkronize etmeye gerek duymaz. Bu, bu makalenin kapsamı dışındadır.

Birbirine Kilitlenmiş İşlevler ve CPU Yeniden Sıralama

Bazen kaynağı alan veya yayımlayan okuma veya yazma işlemi, InterlockedXxx işlevlerinden biri kullanılarak yapılır. Windows'ta bu, işleri basitleştirir; Çünkü Windows'da InterlockedXxx işlevlerinin tümü tam bellek engelleridir. Etkili bir şekilde hem onlardan önce hem de sonra bir CPU bellek engeli vardır, bu da tamamen kendi başlarına tam bir okuma-alma veya yazma-bırakma engeli oldukları anlamına gelir.

Xbox 360'ta InterlockedXxx işlevleri CPU bellek engelleri içermez. Derleyicinin okuma ve yazma işlemlerini yeniden sıralamasını engellerler ancak CPU yeniden sıralamasını engellemezler. Bu nedenle, çoğu durumda Xbox 360'ta InterlockedXxx işlevlerini kullanırken, bu işlevlerin öncesinde veya sonrasında bir __lwsyncyerleştirerek, onları okuma-kazanma veya yazma-serbest bırakma bariyeri haline getirmelisiniz. Kolaylık sağlamak ve daha kolay okunabilirlik için, birçok InterlockedXxx işlevinin Elde Etme (Acquire) ve Serbest Bırakma (Release) sürümleri vardır. Bunlar yerleşik bir bellek engeliyle birlikte gelir. Örneğin, InterlockedIncrementAcquire, tam okuma-kazanç işlevselliği sağlamak için kilitlenmiş bir artırma işlemi gerçekleştirir ve ardından __lwsync bellek bariyerini uygular.

Amacınızı daha belirgin hale getirmek ve bellek engeli yönergelerini doğru yerde almayı kolaylaştırmak için InterlockedXxx işlevlerinin Alma ve Yayın sürümlerini (çoğu Windows'ta da kullanılabilir, performans cezası olmadan) kullanmanız önerilir. Xbox 360'ta bellek engeli olmadan InterlockedXxx kullanımı çok dikkatli bir şekilde incelenmelidir, çünkü bu genellikle bir hatadır.

Bu örnek, InterlockedXxXSList işlevlerinin Alma ve Release sürümlerini kullanarak bir iş parçacığının görevleri veya diğer verileri başka bir iş parçacığına nasıl geçirebileceğini gösterir. InterlockedXxSList işlevleri, paylaşılan tek bağlantılı listeyi kilit olmadan korumaya yönelik bir işlev ailesidir. Unutmayın ki bu işlevlerin Edinme ve Bırakma çeşitleri Windows'ta mevcut değildir, ancak bu işlevlerin normal sürümleri Windows'ta bir tam bellek bariyeridir.

// Declarations for the Task class go here.

// Add a new task to the list using lockless programming.
void AddTask( DWORD ID, DWORD data )
{
    Task* newItem = new Task( ID, data );
    InterlockedPushEntrySListRelease( g_taskList, newItem );
}

// Remove a task from the list, using lockless programming.
// This will return NULL if there are no items in the list.
Task* GetTask()
{
    Task* result = (Task*)
        InterlockedPopEntrySListAcquire( g_taskList );
    return result;
}

Geçici Değişkenler ve Yeniden Sıralama

C++ Standardı geçici değişkenlerin okumalarının önbelleğe alınamayacağını, geçici yazmaların geciktirilemeyeceğini ve geçici okuma ve yazmaların birbirinin dışına taşınamayacağını belirtir. Bu, C++ Standard'daki geçici anahtar sözcüğün amacı olan donanım cihazlarıyla iletişim kurmak için yeterlidir.

Ancak, standardın garantileri çoklu iş parçacığı için volatile kullanmak için yeterli değildir. C++ Standardı, geçici olmayan okuma ve yazma işlemlerine göre derleyicinin geçici olmayan okuma ve yazmaları yeniden sıralamasını durdurmaz ve CPU yeniden sıralamasını engelleme hakkında hiçbir şey yapmaz.

Visual C++ 2005, volatile değişken erişimi için çoklu iş parçacığı dostu semantik tanımlayarak standart C++'ın ötesine geçer. Visual C++ 2005'ten başlayarak geçici değişkenlerden yapılan okumalar okuma alma semantiğine sahip olacak şekilde tanımlanır ve geçici değişkenlere yazma işlemleri yazma-yayın semantiğine sahip olacak şekilde tanımlanır. Bu, derleyicinin bunları geçmiş hiçbir okuma ve yazma işlemini yeniden düzenlemeyeceği ve Windows'ta CPU'ların da bunu yapmamasını sağlayacağı anlamına gelir.

Bu yeni garantilerin yalnızca Visual C++ 2005 ve Visual C++'ın gelecekteki sürümleri için geçerli olduğunu anlamak önemlidir. Diğer satıcıların derleyicileri genellikle Visual C++ 2005'in ek garantileri olmadan farklı semantikler uygular. Ayrıca, Xbox 360'ta, derleyici CPU'un okuma ve yazmaları yeniden sıralamasını önlemek için herhangi bir yönerge eklemez.

Lock-Free Veri Kanalı örneği

Kanal, bir veya daha fazla iş parçacığının daha sonra diğer iş parçacıkları tarafından okunan verileri yazmasına olanak tanıyan bir yapıdır. Bir borunun kilitsiz sürümü, işi iş parçacığından iş parçacığına geçirmenin zarif ve verimli bir yolu olabilir. DirectX SDK, DXUTLockFreePipe.h dosyasında kullanılabilen ve tek okuyuculu, tek yazıcılı kilitsiz bir boru olan LockFreePipesağlar. Aynı LockFreePipe, AtgLockFreePipe.h'deki Xbox 360 SDK'sında kullanılabilir.

LockFreePipe, iki iş parçacığı üretici/tüketici ilişkisine sahip olduğunda kullanılabilir. Üretici iş parçacığı, tüketici iş parçacığının daha sonraki bir tarihte işlemesi için kanala veri yazabilir ve bunu engellemez. Boru dolarsa yazma işlemi başarısız olur ve üretici iş parçacığının daha sonra yeniden denemesi gerekir, ancak bu durum yalnızca üretici iş parçacığı öndeyse gerçekleşir. Kanal boşalırsa, okumalar başarısız olur ve tüketici iş parçacığı daha sonra tekrar denemek zorunda kalır, ancak bu yalnızca tüketici iş parçacığının yapması gereken bir iş olmadığında gerçekleşir. İki iş parçacığı iyi dengelenmişse ve boru yeterince büyükse, boru verileri gecikme veya blok olmadan sorunsuz bir şekilde aktarabilmesine olanak tanır.

Xbox 360 Performansı

Xbox 360'ta eşitleme yönergelerinin ve işlevlerinin performansı, başka hangi kodun çalıştığına bağlı olarak değişir. Kilidin sahibi başka bir iş parçacığıysa kilitlerin alınması çok daha uzun sürer. InterlockedIncrement ve kritik bölüm işlemleri, diğer iş parçacıkları aynı önbellek satırına yazıyorsa çok daha uzun sürer. Mağaza kuyruklarının içeriği performansı da etkileyebilir. Bu nedenle, bu sayıların tümü yalnızca çok basit testlerden oluşturulan yaklaşık değerlerdir:

  • lwsync 33-48 döngü olarak ölçüldü.
  • InterlockedIncrement 225-260 döngü olarak ölçüldü.
  • Kritik bir bölümün edinilmesi veya serbest bırakılması yaklaşık 345 döngü sürmektedir.
  • Bir mutex'in alınması veya serbest bırakılması ortalama 2350 döngü olarak ölçüldü.

Windows Performansı

Windows'daki eşitleme yönergelerinin ve işlevlerinin performansı, işlemci türüne ve yapılandırmasına ve diğer kodun çalıştığına bağlı olarak büyük ölçüde farklılık gösterir. Çok çekirdekli ve çok yuvalı sistemler genellikle eşzamanlama yönergelerini yürütmek için daha uzun süre alır ve eğer bir başka iş parçacığı zaten kilide sahipse, kilidi elde etmek çok daha uzun sürer.

Ancak, çok basit testlerden oluşturulan bazı ölçümler bile yararlıdır:

  • "MemoryBarrier" 20-90 döngü arasında ölçüldü.
  • InterlockedIncrement 36-90 döngü olarak ölçüldü.
  • Kritik bir bölümün alınması veya serbest bırakılması, 40-100 döngü olarak ölçüldü.
  • Bir mutex'in edinilmesi veya serbest bırakılması yaklaşık 750-2500 döngü sürüyordu.

Bu testler Windows XP'de çeşitli işlemcilerde yapılmıştır. Kısa süreler tek işlemcili bir makinede ve daha uzun süreler çok işlemcili bir makinedeydi.

Kilitleri almak ve serbest bırakmak, kilitsiz programlama kullanmaktan daha pahalı olsa da, verileri daha az sıklıkta paylaşmak daha da iyidir, bu nedenle maliyetten tamamen kaçınır.

Performans Düşünceleri

Kritik bir bölümün alınması veya yayınlanması, bir bellek bariyerini, InterlockedXxx işlemini ve özyinelemeyi işlemek ve gerekirse bir mutex'e geri dönmek için bazı ek kontrolleri içerir. Kendi kritik bölümünüzü uygularken dikkatli olmalısınız, çünkü bir mutex'e geri dönmeden kilidin serbest kalmasını bekleyen bir döngüde dönmek önemli ölçüde performans kaybına neden olabilir. Yoğun bir şekilde üstünde çalışılan ancak uzun süre ele geçirilmeyen kritik bölümler için InitializeCriticalSectionAndSpinCount kullanmayı göz önünde bulundurmanız gerekir; böylece işletim sistemi, kritik bölümü elde etmeye çalıştığınızda bir başkası tarafından tutuluyorsa, hemen bir mutex'e başvurmak yerine kritik bölümün kullanılabilir olmasını bir süre bekler. Bir döndürme sayımından yararlanabilecek kritik bölümleri belirlemek için, belirli bir kilit için tipik beklemenin uzunluğunu ölçmek gerekir.

Paylaşılan bir yığın kullanılıyorsa (varsayılan davranış), bellek ayırma ve serbest bırakma işlemlerinin her biri için bir kilit alınır. İş parçacığı sayısı ve ayırma sayısı arttıkça, performans düzeyleri sabitlenir ve giderek azalmaya başlar. İş parçacığı başına yığınları kullanmak veya ayırma sayısını azaltmak, bu kilitleme darboğazını önleyebilir.

Bir iş parçacığı veri oluşturuyorsa ve başka bir iş parçacığı veri tüketiyorsa, verileri sık sık paylaşıyor olabilir. Bu durum, bir iş parçacığı kaynakları yüklerken başka bir iş parçacığı sahneyi işlerse oluşabilir. İşleme iş parçacığı her çizim çağrısında paylaşılan verilere başvuruda bulunursa, kilitleme yükü yüksek olur. Her iş parçacığının, ardından her çerçevede veya daha seyrek olarak senkronize edilen özel veri yapıları bulunursa, çok daha iyi performans elde edilebilir.

Kilitsiz algoritmaların kilit kullanan algoritmalardan daha hızlı olacağı garanti edilmemektedir. Bunları önlemeye çalışmadan önce kilitlerin size gerçekten neden olup olmadığını denetlemeli ve kilitsiz kodunuzun performansı gerçekten geliştirip geliştirmediğini görmek için ölçüm yapmalısınız.

Platform Farkları Özeti

  • InterlockedXxx işlevleri Windows'ta CPU okuma/yazma yeniden sıralamasını engeller ancak Xbox 360'ta bunu engellemez.
  • Visual Studio C++ 2005 kullanılarak geçici değişkenlerin okunması ve yazılması Windows'ta CPU okuma/yazma yeniden sıralamasını engeller, ancak Xbox 360'ta yalnızca derleyici okuma/yazma yeniden sıralamasını engeller.
  • Yazma işlemleri Xbox 360'ta yeniden sıralanır ancak x86 veya x64'te yapılmaz.
  • Okumalar Xbox 360'ta yeniden sıralanır, ancak x86 veya x64'te yalnızca yazma işlemlerine göre ve yalnızca okuma ve yazma işlemleri farklı konumları hedeflediyse yeniden sıralanır.

Öneri

  • Doğru şekilde kullanmak daha kolay olduğundan mümkün olduğunda kilitleri kullanın.
  • Kilitleme maliyetlerinin önemli olmaması için çok sık kilitlemekten kaçının.
  • Uzun duraklardan kaçınmak için kilitleri çok uzun süre elde tutmaktan kaçının.
  • Uygun olduğunda kilitsiz programlama kullanın, ancak kazançların karmaşıklığı haklı çıkardığından emin olun.
  • Ertelenmiş yordam çağrıları ile normal kod arasında veri paylaşırken olduğu gibi diğer kilitlerin yasaklandığı durumlarda kilitsiz programlama veya spin kilitleri kullanın.
  • Yalnızca doğru olduğu kanıtlanmış standart kilitsiz programlama algoritmalarını kullanın.
  • Kilitsiz programlama yaparken, gerektiğinde geçici bayrak değişkenlerini ve bellek engeli yönergelerini kullandığınızdan emin olun.
  • Xbox 360'ta InterlockedXxx kullanırken Acquire ve Release değişkenlerini kullanın.

Referanslar