İ ++ 'nın iş parçacığı güvenli olmadığını duydum, ++ i iş parçacığı güvenli mi?


90

İ ++ 'nın iş parçacığı güvenli bir ifade olmadığını duydum, çünkü derlemede orijinal değeri bir yerde geçici olarak depolamaya, artırmaya ve sonra değiştirerek, bir bağlam anahtarı tarafından kesintiye uğrayabilir.

Ancak, ++ i'yi merak ediyorum. Anlayabildiğim kadarıyla bu, 'r1, r1, 1 ekle' gibi tek bir montaj talimatına indirgenecek ve sadece bir talimat olduğu için, bir bağlam anahtarı ile kesintiye uğramayacak.

Biri açıklayabilir mi? Bir x86 platformunun kullanıldığını varsayıyorum.


Sadece bir soru. İki (veya daha fazla) iş parçacığının böyle bir değişkene erişmesi için ne tür senaryolar gerekir? Dürüst olmak gerekirse burada soruyorum, eleştirmiyorum. Sadece bu saatte, kafam hiçbir şey düşünemiyor.
OscarRyz

5
Bir C ++ sınıfında nesne sayısını koruyan bir sınıf değişkeni?
paxdiablo

1
Bugün başka bir adam bana söylediği için konuyla ilgili güzel bir video: youtube.com/watch?v=mrvAqvtWYb4
Johannes Schaub - litb

1
C / C ++ olarak yeniden etiketlendi; Java burada ele alınmıyor, C # benzer, ancak bu kadar katı tanımlanmış bellek semantiğinden yoksun.
Tim Williscroft

1
@Oscar Reyes Diyelim ki ikiniz de i değişkenini kullanan iki konu var. İplik bir, yalnızca belirli bir noktada ipliği arttırırsa ve diğeri yalnızca başka bir noktada ipliği azaltırsa, iplik güvenliği konusunda endişelenmeniz gerekir.
samoz

Yanıtlar:


158

Yanlış duydun. İyi olabilir "i++", belirli bir derleyici ve belirli işlemci mimarisi için evreli ama hiç standartlarda zorunlu değil. Aslında, çoklu iş parçacığı ISO C veya C ++ standartlarının (a) bir parçası olmadığından, derleneceğini düşündüğünüz şeye dayanarak hiçbir şeyin iş parçacığı açısından güvenli olduğunu düşünemezsiniz.

Aşağıdakiler ++igibi gelişigüzel bir sıraya göre derlemek oldukça mümkündür :

load r0,[i]  ; load memory into reg 0
incr r0      ; increment reg 0
stor [i],r0  ; store reg 0 back to memory

bu, bellek artırma talimatları olmayan (hayali) CPU'mda iş parçacığı açısından güvenli olmazdı. Veya akıllı olabilir ve onu şu şekilde derleyebilir:

lock         ; disable task switching (interrupts)
load r0,[i]  ; load memory into reg 0
incr r0      ; increment reg 0
stor [i],r0  ; store reg 0 back to memory
unlock       ; enable task switching (interrupts)

nerede lockdevre dışı bırakır ve unlockkesmeleri mümkün kılar. Ancak, o zaman bile, belleği paylaşan bu CPU'lardan birden fazlasına sahip bir mimaride bu, iş parçacığı açısından güvenli lockolmayabilir (yalnızca bir CPU için kesintileri devre dışı bırakabilir).

Dilin kendisi (veya dilde yerleşik değilse bunun için kitaplıklar) iş parçacığı güvenli yapılar sağlayacaktır ve bunları hangi makine kodunun üretileceğini anlamanıza (veya muhtemelen yanlış anlamanıza) bağlı olmak yerine kullanmalısınız.

Java synchronizedve pthread_mutex_lock()(bazı işletim sistemlerinde C / C ++ için mevcuttur) gibi şeyler, (a) ' ya bakmanız gereken şeylerdir .


(a) Bu soru, C11 ve C ++ 11 standartları tamamlanmadan önce sorulmuştur. Bu yinelemeler, atomik veri türleri de dahil olmak üzere dil spesifikasyonlarına iş parçacığı desteği getirmiştir (ancak bunlar ve genel olarak iş parçacıkları en azından C'de isteğe bağlıdır ).


8
Bunun platforma özgü bir sorun olmadığını vurgulayan +1, net bir cevaptan bahsetmiyorum bile ...
RBerteig

2
C gümüş rozetiniz için tebrikler :)
Johannes Schaub - litb

Bence hiçbir modern işletim sisteminin kullanıcı modu programlarına kesintileri kapatmak için yetki vermediğini ve pthread_mutex_lock () C'nin bir parçası olmadığını kesinleştirmelisiniz.
Bastien Léonard

@Bastien, bellek artırma talimatı olmayan bir CPU üzerinde hiçbir modern işletim sistemi çalışmaz :-) Ama sizin
fikriniz

5
@Bastien: Bull. RISC işlemcileri genellikle bellek artırma yönergelerine sahip değildir. Yükleme / ekleme / depolama üçlüsü, örneğin PowerPC'de bunu nasıl yapacağınızdır.
derobert

42

++ i veya i ++ hakkında genel bir açıklama yapamazsınız. Neden? 32 bitlik bir sistemde 64 bitlik bir tamsayıyı artırmayı düşünün. Altta yatan makinenin dört kelimelik bir "yükleme, artırma, saklama" talimatı olmadığı sürece, bu değeri artırmak birden fazla talimat gerektirecektir ve bunların herhangi biri bir iş parçacığı bağlam anahtarı ile kesilebilir.

Ayrıca, ++iher zaman "değere bir ekleme" değildir. C gibi bir dilde, işaretçiyi artırmak aslında işaret edilen şeyin boyutunu ekler. Yani, i32 baytlık bir yapıya işaretçi ise ++i32 bayt ekler. Hemen hemen tüm platformların atomik olan bir "bellek adresinde artış değeri" talimatı varken, hepsinin atomik "bellek adresindeki değere rastgele değer ekle" talimatı yoktur.


35
Elbette, kendinizi sıkıcı 32-bit tam sayılarla sınırlamazsanız, C ++, ++ gibi bir dilde, gerçekten bir veritabanındaki bir değeri güncelleyen bir web hizmetine çağrı olabilirim.
Eclipse

16

Her ikisi de iş parçacığı açısından güvenli değil.

Bir CPU doğrudan bellekle matematik yapamaz. Bunu dolaylı olarak değeri bellekten yükleyerek ve CPU yazmaçları ile matematik yaparak yapar.

i ++

register int a1, a2;

a1 = *(&i) ; // One cpu instruction: LOAD from memory location identified by i;
a2 = a1;
a1 += 1; 
*(&i) = a1; 
return a2; // 4 cpu instructions

++ i

register int a1;

a1 = *(&i) ; 
a1 += 1; 
*(&i) = a1; 
return a1; // 3 cpu instructions

Her iki durumda da, tahmin edilemeyen i değeriyle sonuçlanan bir yarış koşulu vardır.

Örneğin, her biri sırasıyla a1, b1 yazmaçlarını kullanan iki eşzamanlı ++ i iş parçacığı olduğunu varsayalım. Ve bağlam değiştirme ile aşağıdaki gibi yürütülür:

register int a1, b1;

a1 = *(&i);
a1 += 1;
b1 = *(&i);
b1 += 1;
*(&i) = a1;
*(&i) = b1;

Sonuçta i + 2 olmaz, i + 1 olur ki bu yanlıştır.

Bunu düzeltmek için, moden CPU'lar bir bağlam anahtarlamasının devre dışı bırakıldığı aralık sırasında bir tür LOCK, UNLOCK cpu talimatları sağlar.

Win32'de, iş parçacığı güvenliği için i ++ yapmak üzere InterlockedIncrement () kullanın. Mutex'e güvenmekten çok daha hızlı.


6
"Bir CPU doğrudan bellekle matematik işlemi yapamaz" - Bu doğru değildir. Önce bir kayda yüklemeye gerek kalmadan bellek öğeleri üzerinde "doğrudan" matematik işlemi yapabileceğiniz CPU'lar vardır. Örneğin. MC68000
darklon

1
LOCK ve UNLOCK CPU komutlarının bağlam anahtarlarıyla ilgisi yoktur. Önbellek hatlarını kilitlerler.
David Schwartz

11

Çok çekirdekli bir ortamda iş parçacıkları arasında bir int bile paylaşıyorsanız, uygun bellek engellerine ihtiyacınız vardır. Bu, birbirine kenetlenmiş talimatların kullanılması (örneğin, win32'deki InterlockedIncrement'e bakın) veya belirli iş parçacığı güvenli garantiler veren bir dil (veya derleyici) kullanılması anlamına gelebilir. CPU seviyesinde talimat yeniden sıralama, önbellekler ve diğer sorunlar ile, bu garantilere sahip değilseniz, iş parçacıkları arasında paylaşılan hiçbir şeyin güvenli olduğunu varsaymayın.

Düzenleme: Çoğu mimaride varsayabileceğiniz bir şey, doğru şekilde hizalanmış tek kelimelerle uğraşıyorsanız, birbirine karıştırılmış iki değerin bir kombinasyonunu içeren tek bir kelimeyle sonuçlanmayacağınızdır. Üst üste iki yazma işlemi olursa, biri kazanır ve diğeri atılır. Dikkatli olursanız, bundan faydalanabilir ve tek yazar / çoklu okuyucu durumunda ++ i veya i ++ 'nın iş parçacığı açısından güvenli olduğunu görebilirsiniz.


Tam erişimin (okuma / yazma) atomik olduğu ortamlarda gerçekten yanlış. Bellek engellerinin olmaması bazen eski veriler üzerinde çalıştığınız anlamına gelse de, bu tür ortamlarda çalışabilen algoritmalar vardır.
MSalters

2
Sadece atomikliğin iplik güvenliğini garanti etmediğini söylüyorum. Kilitsiz veri yapıları veya algoritmalar tasarlayacak kadar akıllıysanız, devam edin. Ancak yine de derleyicinizin size vereceği garantilerin ne olduğunu bilmeniz gerekir.
Eclipse

10

C ++ 'da atomik bir artış istiyorsanız, C ++ 0x kitaplıklarını ( std::atomicveri türü) veya TBB gibi bir şeyi kullanabilirsiniz.

Bir zamanlar GNU kodlama kılavuzları, bir kelimeye uyan veri türlerini güncellemenin "genellikle güvenli" olduğunu, ancak bu tavsiyenin SMP makineleri için yanlış, bazı mimariler için yanlış ve optimize edici bir derleyici kullanırken yanlış olduğunu söyledi.


"Tek kelimelik veri türü güncelleniyor" açıklamasını netleştirmek için:

Bir SMP makinesindeki iki CPU'nun aynı döngüde aynı bellek konumuna yazması ve ardından değişikliği diğer CPU'lara ve önbelleğe yaymaya çalışması mümkündür. Yalnızca tek bir veri kelimesi yazılsa bile, böylece yazmalar yalnızca bir döngüde tamamlanır, bunlar aynı anda gerçekleşir, böylece hangi yazmanın başarılı olacağını garanti edemezsiniz. Kısmen güncellenmiş veriler almayacaksınız, ancak bu durumu ele almanın başka yolu olmadığı için bir yazma kaybolacak.

Karşılaştırın ve birden fazla CPU arasında doğru koordinatları değiştirin, ancak tek kelimelik veri türlerinin her değişken atamasının karşılaştır ve değiştir'i kullanacağına inanmak için hiçbir neden yoktur.

Bir optimize derleyici etkilemez ederken nasıl bir yük / mağaza derlenmiş, bu değiştirebilir zaman yük / mağaza olur sen senin okur ve (onlar kaynak kodunda görünen aynı sırayla gerçekleşmesi yazıyor bekliyorsanız ciddi sorun neden en ünlüsü çift kontrollü kilitleme vanilya C ++ 'da çalışmaz.

NOT Orijinal cevabım Intel 64 bit mimarisinin 64 bit verilerle uğraşırken bozulduğunu da söyledi. Bu doğru değil, bu yüzden cevabı düzenledim, ancak düzenlemem PowerPC yongalarının kırıldığını iddia etti. Bu, kayıtlara anlık değerleri (yani sabitleri) okurken doğrudur ( liste 2 ve liste 4 altındaki "Yükleme işaretçileri" adlı iki bölüme bakın). Ancak bir döngüde bellekten veri yüklemek için bir talimat var ( lmw), bu yüzden cevabımın bu kısmını kaldırdım.


Verileriniz doğal olarak hizalanmışsa ve SMP ve optimize edici derleyicilerle bile doğru boyuttaysa, okuma ve yazma işlemleri çoğu modern CPU'da atomiktir. Yine de, özellikle 64 bit makinelerde pek çok uyarı vardır, bu nedenle verilerinizin her makinedeki gereksinimleri karşıladığından emin olmak zahmetli olabilir.
Dan Olson

Güncellediğiniz için teşekkürler. Doğru, okuma ve yazma, yarı tamamlanamayacaklarını belirttiğiniz için atomiktir, ancak yorumunuz bu gerçeğe pratikte nasıl yaklaştığımızı vurgulamaktadır. Aynı hafıza engelleri ile aynı, operasyonun atomik doğasını değil, pratikte ona nasıl yaklaştığımızı etkilerler.
Dan Olson


4

Programlama diliniz iş parçacıkları hakkında hiçbir şey söylemese de çok iş parçacıklı bir platformda çalışıyorsa, herhangi bir dil yapısı nasıl iş parçacığı açısından güvenli olabilir?

Diğerlerinin de belirttiği gibi: değişkenlere çok iş parçacıklı erişimi platforma özel çağrılarla korumanız gerekir.

Platformun özgüllüğünü soyutlayan kitaplıklar var ve yaklaşan C ++ standardı, bellek modelini iş parçacıklarıyla başa çıkmak için uyarladı (ve böylece iş parçacığı güvenliğini garanti edebilir).


4

Değeri doğrudan bellekte artırarak tek bir montaj komutuna indirgenmiş olsa bile, yine de iş parçacığı güvenli değildir.

Bellekteki bir değeri artırırken, donanım bir "oku-değiştir-yaz" işlemi yapar: değeri bellekten okur, artırır ve belleğe geri yazar. X86 donanımının doğrudan bellek üzerinde artırma yolu yoktur; RAM (ve önbellekler) değerleri değiştiremez, yalnızca okuyabilir ve saklayabilir.

Şimdi, ya ayrı soketlerde bulunan ya da tek bir soketi paylaşan (paylaşılan bir önbellek ile veya olmadan) iki ayrı çekirdeğiniz olduğunu varsayalım. İlk işlemci değeri okur ve güncellenmiş değeri geri yazmadan önce ikinci işlemci onu okur. Her iki işlemci de değeri geri yazdıktan sonra, iki kez değil, yalnızca bir kez artırılacaktır.

Bu sorunu önlemenin bir yolu var; x86 işlemcileri (ve bulacağınız çoğu çok çekirdekli işlemci), donanımdaki bu tür çatışmayı algılayabilir ve sıralayabilir, böylece tüm okuma-değiştirme-yazma sırası atomik görünür. Ancak, bu çok maliyetli olduğu için, yalnızca kod tarafından talep edildiğinde, x86'da genellikle LOCKönek aracılığıyla yapılır . Diğer mimariler bunu benzer sonuçlarla başka şekillerde yapabilir; örneğin, yük bağlantılı / saklama koşullu ve atomik karşılaştırma ve değiştirme (son x86 işlemcilerinde de bu sonuncusu var).

Kullanmanın volatileburada yardımcı olmadığını unutmayın ; yalnızca derleyiciye değişkenin harici olarak değiştirilebileceğini söyler ve bu değişkeni okur, bir kayıtta önbelleğe alınmamalı veya optimize edilmemelidir. Derleyicinin atomik ilkelleri kullanmasını sağlamaz.

En iyi yol, atomik ilkelleri kullanmaktır (derleyiciniz veya kitaplıklarınız varsa) veya artışı doğrudan montajda (doğru atom talimatlarını kullanarak) yapmaktır.


2

Asla bir artımın atomik bir işleme derleneceğini varsaymayın. InterlockedIncrement veya hedef platformunuzda bulunan benzer işlevleri kullanın.

Düzenleme: Bu özel soruyu biraz önce araştırdım ve X86'daki artış tek işlemcili sistemlerde atomiktir, ancak çok işlemcili sistemlerde değildir. Kilit önekini kullanmak onu atomik yapabilir, ancak sadece InterlockedIncrement'i kullanmak çok daha taşınabilir.


1
InterlockedIncrement () bir Windows işlevidir; tüm Linux kutularım ve modern OS X makinelerim x64 tabanlıdır, bu nedenle InterlockedIncrement () 'nin x86 kodunun sahte olduğundan' çok daha taşınabilir 'olduğunu söylemek.
Pete Kirkham

C'nin montajdan çok daha taşınabilir olmasıyla aynı anlamda çok daha taşınabilir. Buradaki amaç, belirli bir işlemci için özel olarak oluşturulmuş montajlara güvenmekten kendinizi korumaktır. Endişeniz diğer işletim sistemleri ise, InterlockedIncrement kolayca kapatılır.
Dan Olson

2

X86'daki bu derleme dersine göre , bir bellek konumuna atomik olarak bir kayıt ekleyebilirsiniz , bu nedenle potansiyel olarak kodunuz atomik olarak '++ i' ou 'i ++' çalıştırabilir. Ancak başka bir gönderide belirtildiği gibi, C ansi atomikliği '++' işlemine uygulamaz, bu nedenle derleyicinizin ne üreteceğinden emin olamazsınız.


1

1998 C ++ standardının, bir sonraki standart (bu yıl veya sonraki) yapmasına rağmen, iş parçacıkları hakkında söyleyecek hiçbir şeyi yoktur. Bu nedenle, uygulamaya atıfta bulunmadan işlemlerin iş parçacığı güvenliği hakkında akıllıca bir şey söyleyemezsiniz. Bu sadece kullanılan işlemci değil, derleyici, işletim sistemi ve iş parçacığı modelinin birleşimidir.

Aksine dokümantasyon olmadığından, özellikle çok çekirdekli işlemcilerde (veya çok işlemcili sistemlerde) herhangi bir eylemin iş parçacığı için güvenli olduğunu varsaymam. İş parçacığı senkronizasyon problemlerinin sadece kazara ortaya çıkması muhtemel olduğundan testlere de güvenmezdim.

Kullandığınız belirli sistem için olduğunu söyleyen belgeleriniz olmadığı sürece hiçbir şey iş parçacığı açısından güvenli değildir.


1

İ iş parçacığı yerel depolamaya atın; atomik değil, ama o zaman önemli değil.


1

AFAIK, C ++ standardına göre, intatomik bir'e okur / yazar .

Ancak, bunun yaptığı tek şey, bir veri yarışıyla ilişkili tanımlanmamış davranıştan kurtulmaktır.

Ancak, her iki iş parçacığı da artmaya çalışırsa, yine de bir veri yarışı olacaktır i.

Aşağıdaki senaryoyu hayal edin:

Let i = 0başlangıçta:

İş Parçacığı A değeri bellekten okur ve kendi önbelleğinde depolar. İplik A, değeri 1 artırır.

B İş Parçacığı, değeri bellekten okur ve kendi önbelleğinde depolar. Diş B, değeri 1 artırır.

Bunların hepsi tek bir iş parçacığı i = 2ise hafızaya girersiniz.

Ancak her iki iş parçacığı ile, her iş parçacığı değişikliklerini yazar ve böylece İş Parçacığı A i = 1belleğe geri yazar ve İş Parçacığı B i = 1belleğe yazar .

İyi tanımlanmış, bir nesnenin kısmi bir tahribatı veya yapımı veya herhangi bir şekilde yırtılması yok, ama yine de bir veri yarışı.

Atomik olarak artırmak iiçin şunları kullanabilirsiniz:

std::atomic<int>::fetch_add(1, std::memory_order_relaxed)

Rahat sıralama kullanılabilir çünkü bu işlemin nerede gerçekleşeceği umrumuzda değil, tek umursadığımız artırma işleminin atomik olmasıdır.


0

"Bu sadece bir talimat, bir bağlam anahtarı ile kesintisiz olacaktır" diyorsunuz. - hepsi tek bir CPU için iyi ve iyi, peki ya çift çekirdekli bir CPU? Böylece, aynı değişkene aynı anda herhangi bir bağlam anahtarı olmadan erişen iki iş parçacığına sahip olabilirsiniz.

Dili bilmeden cevap, onu test etmektir.


4
Bir şeyin iş parçacığı açısından güvenli olup olmadığını test ederek öğrenemezsiniz - iş parçacığı oluşturma sorunları milyonda bir olabilir. Belgelerinize bakarsınız. Belgeleriniz tarafından iş parçacığı güvencesi garanti edilmiyorsa, değildir.
Eclipse

2
@ Josh ile burada anlaşın. Bir şey yalnızca temel kodun analizi yoluyla matematiksel olarak kanıtlanabiliyorsa iş parçacığı açısından güvenlidir. Hiçbir test buna yaklaşmaya başlayamaz.
Rex M

Bu son cümleye kadar harika bir cevaptı.
Rob K

0

Bence "i ++" ifadesi bir ifadedeki tek ifade ise, "++ i" ile eşdeğerdir, derleyici geçici bir değer tutmayacak kadar akıllıdır, vb. Dolayısıyla, bunları birbirinin yerine kullanabilirseniz (aksi takdirde kazanırsınız) Hangisini kullanacağınızı sormayın), neredeyse aynı oldukları için hangisini kullanırsanız kullanın (estetik hariç).

Her neyse, artış operatörü atomik olsa bile, bu doğru kilitleri kullanmazsanız hesaplamanın geri kalanının tutarlı olacağını garanti etmez.

Kendiniz denemek istiyorsanız, N iş parçacığının aynı anda paylaşılan bir değişkeni M kez artırdığı bir program yazın ... değer N * M'den küçükse, bazı artışların üzerine yazılır. Hem ön artırma hem de sonradan artırma ile deneyin ve bize söyleyin ;-)


0

Bir sayaç için, hem kilitlenmeyen hem de iş parçacığı için güvenli olan karşılaştırma ve takas deyimini kullanmanızı öneririm.

İşte Java'da:

public class IntCompareAndSwap {
    private int value = 0;

    public synchronized int get(){return value;}

    public synchronized int compareAndSwap(int p_expectedValue, int p_newValue){
        int oldValue = value;

        if (oldValue == p_expectedValue)
            value = p_newValue;

        return oldValue;
    }
}

public class IntCASCounter {

    public IntCASCounter(){
        m_value = new IntCompareAndSwap();
    }

    private IntCompareAndSwap m_value;

    public int getValue(){return m_value.get();}

    public void increment(){
        int temp;
        do {
            temp = m_value.get();
        } while (temp != m_value.compareAndSwap(temp, temp + 1));

    }

    public void decrement(){
        int temp;
        do {
            temp = m_value.get();
        } while (temp > 0 && temp != m_value.compareAndSwap(temp, temp - 1));

    }
}

Bir test_and_set işlevine benzer görünüyor.
samoz

1
"Kilitlenmiyor" yazdınız, ancak "senkronize" kilitlemek anlamına gelmiyor mu?
Corey Trager
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.