Tek çekirdekli CPU'da birden fazla iş parçacığının neden kilitlere ihtiyaç duyduğunu açıklayabilir misiniz?


18

Bu iş parçacıklarının tek çekirdekli CPU'da çalıştığını varsayın. CPU olarak bir döngüde sadece bir komut çalıştırın. Bununla birlikte, cpu kaynağını paylaştıklarını bile düşündüm. ancak bilgisayar bir kez bir talimat sağlamak. Peki kilit çoklu kullanım için gerekli değil mi?


Çünkü yazılım işlem belleği henüz ana akım değildir.
dan_waterworth

@dan_waterworth Yazılım işlem belleği önemsiz karmaşıklık düzeylerinde kötü başarısız olduğu için, yani? ;)
Mason Wheeler

Eminim Rich Hickey buna katılmaz.
Robert Harvey

@MasonWheeler, önemsiz olmayan kilitleme inanılmaz derecede iyi çalışıyor ve izlemesi zor olan ince hataların kaynağı olmadı mı? STM önemsiz olmayan karmaşıklık düzeyleriyle iyi çalışır, ancak çekişme olduğunda sorunludur. Bu gibi durumlarda, böyle bir şey bu STM bir daha kısıtlayıcı şeklidir, daha iyidir. Btw, başlık değişikliği ile neden yaptığım gibi yorum yaptığımı çözmek için zamanımı aldı.
dan_waterworth

Yanıtlar:


32

Bu en iyi şekilde bir örnekle gösterilmiştir.

Paralel olarak birden çok kez gerçekleştirmek istediğimiz basit bir görevimiz olduğunu ve örneğin bir web sayfasındaki isabetleri saymak gibi görevin küresel olarak gerçekleştirilme sayısını takip etmek istediğimizi varsayalım.

Her iş parçacığı sayımı artırdığı noktaya geldiğinde, yürütme aşağıdaki gibi görünecektir:

  1. Bellekten bir işlemci kaydına isabet sayısını okuyun
  2. Bu sayıyı artır.
  3. Bu numarayı tekrar belleğe yaz

Her iş parçacığının bu işlemin herhangi bir noktasında askıya alınabileceğini unutmayın. Bu nedenle, A iş parçacığı 1. adımı gerçekleştirir ve daha sonra askıya alınırsa, B iş parçacığının her üç adımı gerçekleştirmesini takiben, A iş parçacığı devam ettiğinde, kayıtlarında yanlış isabet sayısı olur: kayıtları geri yüklenir, eski sayıyı mutlu bir şekilde artırır. ve bu artan sayıyı saklayın.

Ayrıca, A iş parçacığının askıya alındığı sırada herhangi bir sayıda başka iş parçacığı çalıştırılmış olabilir, bu nedenle sonunda yazılan A iş parçacığı sayısı doğru sayımın çok altında olabilir.

Bu nedenle, bir iş parçacığı 1. adımı gerçekleştirirse, başka bir iş parçacığının 1. adımı gerçekleştirmesine izin verilmeden önce 3. adımı gerçekleştirmesi gerekir; bu işlem, bu işleme başlamadan önce tek bir kilit almak için bekleyen tüm iş parçacıkları tarafından gerçekleştirilebilir. ve kilidin yalnızca işlem tamamlandıktan sonra serbest bırakılması, böylece kodun bu "kritik bölümü" yanlış araya sokulamaz ve bu da yanlış saymaya neden olur.

Peki ya operasyon atomik olsaydı?

Evet, arttırma işleminin atomik olduğu büyülü tek boynuzlu atlar ve gökkuşağı topraklarında, yukarıdaki örnek için kilitleme gerekli değildir.

Bununla birlikte, büyülü tek boynuzlu atlar ve gökkuşağı dünyasında çok az zaman harcadığımızı fark etmek önemlidir. Hemen hemen her programlama dilinde, arttırma işlemi yukarıdaki üç adıma ayrılır. Bunun nedeni, işlemci bir atomik artış işlemini desteklese bile, bu işlem çok daha pahalıdır: bellekten okumak, numarayı değiştirmek ve tekrar belleğe yazmak zorundadır ... ve genellikle atomik artış işlemi, başarısız olabilir, yani yukarıdaki basit sıra bir döngü ile değiştirilmelidir (aşağıda göreceğimiz gibi).

Çok iş parçacıklı kodda bile, birçok değişken tek bir iş parçacığında yerel tutulduğu için, programlar her değişkenin tek bir iş parçacığında yerel olduğunu varsayarlar ve programcıların iş parçacıkları arasındaki paylaşılan durumu korumalarına izin verirlerse çok daha verimlidirler. Özellikle atomik işlemlerin, daha sonra göreceğimiz gibi, diş açma sorunlarını çözmek için genellikle yeterli olmadığı göz önüne alındığında.

Uçucu değişkenler

Bu sorun için kilitlerden kaçınmak istiyorsak, ilk örneğimizde tasvir edilen adımların aslında modern derlenmiş kodda gerçekleşen şey olmadığını fark etmeliyiz. Derleyiciler yalnızca bir iş parçacığının değişkeni değiştirdiğini varsaydığından, işlemci iş parçacığı başka bir şey için gerekli olana kadar her iş parçacığı değişkenin kendi önbelleğe alınmış kopyasını tutacaktır. Önbelleğe alınmış kopyaya sahip olduğu sürece, belleğe geri dönüp tekrar okumak zorunda olmadığını varsayar (bu pahalı olur). Ayrıca bir kayıt defterinde tutulduğu sürece değişkeni belleğe geri yazmazlar.

İlk örnekte verdiğimiz duruma (yukarıda tanımladığımız tüm aynı diş açma problemleriyle) değişkeni uçucu olarak işaretleyerek geri dönebiliriz , bu da derleyiciye bu değişkenin başkaları tarafından değiştirildiğini söyler ve bu nedenle okunması gerekir veya erişildiğinde veya değiştirildiğinde bellek belleğine yazılır.

Dolayısıyla, uçucu olarak işaretlenmiş bir değişken bizi atomik artış operasyonlarının ülkesine götürmeyecek, sadece bizi zaten düşündüğümüz kadar yaklaştırıyor.

Artışı atomik yapmak

Bir değişken değişken kullandığımızda, çoğu modern CPU'nun desteklediği (genellikle karşılaştırma ve ayarlama veya karşılaştırma ve takas adı verilir) düşük seviyeli bir koşullu ayar işlemi kullanarak artış operasyonumuzu atomik hale getirebiliriz . Bu yaklaşım örneğin Java'nın AtomicInteger sınıfında uygulanır:

197       /**
198        * Atomically increments by one the current value.
199        *
200        * @return the updated value
201        */
202       public final int incrementAndGet() {
203           for (;;) {
204               int current = get();
205               int next = current + 1;
206               if (compareAndSet(current, next))
207                   return next;
208           }
209       }

Yukarıdaki döngü, 3. adım başarılı olana kadar aşağıdaki adımları tekrar tekrar gerçekleştirir:

  1. Geçici bir değişkenin değerini doğrudan bellekten okuyun.
  2. Bu değeri artırın.
  3. Değeri (ana bellekte), yalnızca ana bellekteki mevcut değeri, özel bir atomik işlem kullanarak ilk başta okuduğumuz değerle aynıysa değiştirin.

Adım 3 başarısız olursa (adım 1'den sonra değer farklı bir iş parçacığı tarafından değiştirildiğinden), değişkeni doğrudan ana bellekten yeniden okur ve yeniden dener.

Karşılaştırma ve değiştirme işlemi pahalı olsa da, bu durumda kilitleme kullanmaktan biraz daha iyidir, çünkü adım 1'den sonra bir iplik askıya alınırsa, adım 1'e ulaşan diğer ipliklerin ilk ipliği engellemesi ve beklemesi gerekmez. pahalı içerik geçişini önleyebilir. İlk iş parçacığı devam ettiğinde, değişkeni yazmak için ilk denemesinde başarısız olur, ancak yine de kilitleme ile gerekli olabilecek bağlam anahtarından daha ucuz olan değişkeni yeniden okuyarak devam edebilir.

Böylece, gerçek kilitleri kullanmadan karşılaştırma ve takas yoluyla atomik artışların (veya tek bir değişken üzerindeki diğer işlemlerin) ülkesine gidebiliriz.

Peki kilitleme ne zaman kesinlikle gereklidir?

Bir atomik işlemde birden fazla değişkeni değiştirmeniz gerekiyorsa, kilitleme gerekli olacaktır, bunun için özel bir işlemci talimatı bulamazsınız.

Tek bir değişken üzerinde çalıştığınız ve başarısız olmak ve değişkeni okumak ve yeniden başlamak zorunda kaldığınız her iş için hazır olduğunuz sürece, karşılaştırma ve takas yeterince iyi olacaktır.

Her bir iş parçacığının önce bir X değişkenine 2 eklediği ve ardından X'i ikiyle çarptığı bir örneği ele alalım.

X başlangıçta bir ve iki iş parçacığı çalışıyorsa, sonucun (((1 + 2) * 2) + 2) * 2 = 16 olmasını bekliyoruz.

Bununla birlikte, dişler serpiştirilirse, tüm işlemler atomik olsa bile, bunun yerine önce her iki ekleme de gerçekleşebilir ve çarpmalar sonra gelir (1 + 2 + 2) * 2 * 2 = 20.

Bunun nedeni, çarpma ve toplama işlemlerinin değişmeli işlemler olmamasıdır.

İşlemlerin kendileri atomik olmak yeterli değil, operasyonların birleşimini atomik yapmalıyız.

İşlemi serileştirmek için kilitleme kullanarak yapabiliriz, ya da hesaplamaya başladığımızda X değerini depolamak için bir yerel değişken, ara adımlar için ikinci bir yerel değişken kullanabilir ve daha sonra karşılaştır ve değiştir özelliğini kullanabiliriz. yalnızca X'in geçerli değeri X'in orijinal değeri ile aynıysa yeni bir değer belirleyin. Başarısız olursak, X'i okuyarak ve hesaplamaları tekrar yaparak yeniden başlamamız gerekir.

Dahil olan birkaç değiş tokuş vardır: hesaplamalar uzadıkça, devam eden iş parçacığının askıya alınması çok daha olası hale gelir ve devam etmeden önce değer başka bir iş parçacığı tarafından değiştirilir, yani hatalar çok daha olası hale gelir ve bu da israfa yol açar işlemci zamanı. Çok uzun çalışma hesaplamaları olan çok sayıda iş parçacığının aşırı durumunda, değişkeni okumak ve hesaplamalara katılmak için 100 iş parçacığımız olabilir, bu durumda yeni değeri yazarken yalnızca ilk bitirmeyi başarır, diğer 99 yine de hesaplamalarını tamamlayın, ancak tamamlandığında değeri güncelleyemediklerini keşfedin ... bu noktada her biri değeri okuyacak ve hesaplamaya yeniden başlayacaklardır. Kalan 99 iş parçacığının da aynı sorunu tekrar etmesini ve büyük miktarda işlemci zamanını boşa harcamasını isterdik.

Kritik bölümün kilitlerle tam serileştirilmesi bu durumda çok daha iyi olurdu: 99 diş, kilidi alamadıklarında askıya alınır ve her bir ipliği kilitleme noktasına varış sırasına göre çalıştırırdık.

Serileştirme kritik değilse (artan durumumuzda olduğu gibi) ve sayının güncellenmesi başarısız olursa kaybedilecek hesaplamalar minimumsa, karşılaştırma ve takas işleminin kullanılmasıyla kazanılması önemli bir avantaj olabilir, çünkü bu işlem kilitlemeden daha ucuzdur.


ama karşı artış atomik ise, kilit gerekli miydi?
pythonee

@pythonee: eğer karşı artış atomsa, muhtemelen olmaz. Ancak, makul büyüklükteki herhangi bir çok iş parçacıklı programda, paylaşılan bir kaynak üzerinde yapılacak atom olmayan görevleriniz olacaktır.
Doc Brown

1
Artış atomunu yapmak için doğal bir derleyici kullanmadığınız sürece, muhtemelen değildir.
Mike Larsen

Evet, okuma / değiştirme (artış) / yazma atomikse, bu işlem için kilit gereksizdir. DEC-10 AOSE (bir tane ekleyin ve sonuç == 0 ise atla) komutu özellikle test ve semafor olarak kullanılabilmesi için atomik olarak yapılmıştır. Kılavuz, yeterince iyi olduğunu belirtiyor çünkü makinenin 36 bitlik bir kaydı baştan sona yuvarlaması için birkaç gün sürekli saymayı gerektiriyor. ŞİMDİ, ancak yaptığınız her şey "belleğe bir tane eklemek" olmayacaktır.
John R. Strohm

Cevabımı şu endişelerin bazılarını ele almak için güncelledim: evet, işlemi atom haline getirebilirsiniz, ancak hayır, onu destekleyen mimarilerde bile, varsayılan olarak atomik olmayacak ve atomikliğin olmadığı durumlar var yeterli ve tam serileştirme gereklidir. Kilitleme, tam serileştirme sağlamak için bildiğim tek mekanizma.
Theodore Murdock

4

Bu alıntıyı düşünün:

Bazı insanlar, bir sorunla karşı karşıya kaldıklarında, "Biliyorum, iplik kullanacağım" diye düşünüyorlar ve sonra iki tanesi poblesms'i koruyor

herhangi bir zamanda bir CPU üzerinde 1 komut çalışsa bile, bilgisayar programları atomik montaj talimatlarından çok daha fazlasını içerir. Örneğin, konsola (veya bir dosyaya) yazmak, istediğiniz gibi çalıştığından emin olmak için kilitlemeniz gerektiği anlamına gelir.


Alıntı düzenli ifadeler olduğunu düşündüm, konu değil mi?
user16764

3
Alıntı bana iş parçacıkları için çok daha uygulanabilir görünüyor (iş parçacığı sorunları nedeniyle kelimeler / karakterler bozuk yazdırılıyor). Ancak şu anda çıktıda kodun üç sorunu olduğunu gösteren fazladan bir "s" var.
Theodore Murdock

1
Bu bir yan etkidir. Çok zaman zaman 1 artı 1 ekleyebilir ve 4294967295 alabilirsiniz :)
gbjbaanb

3

Kilitlemeyi açıklamaya çalışan birçok cevap görünüyor, ancak OP'nin ihtiyaç duyduğu şey, çoklu görevin gerçekte ne olduğunun bir açıklaması olduğunu düşünüyorum.

Bir CPU ile bile bir sistemde çalışan birden fazla iş parçacığınız varsa, bu iş parçacıklarının nasıl programlanacağını belirleyen iki ana yöntem vardır (yani tek çekirdekli CPU'nuzda çalıştırılacak şekilde yerleştirilmiştir):

  • Kooperatif Çoklu Görev - Win9x'te kullanılan her uygulamanın açıkça kontrolü bırakması gerekir. Bu durumda, A İş parçacığı bir algoritma yürüttüğü sürece kilitleme konusunda endişelenmenize gerek yoktur, asla kesilmeyeceği garanti edilir
  • Önleyici Çoklu Görev - Çoğu modern işletim sisteminde kullanılır (Win2k ve üstü). Bu, zaman dilimlerini kullanır ve işlerini yapıyor olsalar bile iş parçacıklarını keser. Bu çok daha sağlamdır, çünkü tek bir iplik asla tüm makinenizi asamaz, bu da kooperatif çoklu görev ile gerçek bir olasılıktır. Öte yandan, şimdi kilitler hakkında endişelenmeniz gerekir, çünkü herhangi bir zamanda, iş parçacıklarınızdan biri kesilebilir (yani önlenebilir) ve işletim sistemi çalışacak farklı bir iş parçacığı zamanlayabilir. Çok iş parçacıklı uygulamaları bu davranışla kodlarken, her kod satırı (hatta her komut) arasında farklı bir iş parçacığının çalışabileceğini dikkate almalısınız. Şimdi, tek bir çekirdekle bile verilerinizin tutarlı durumunu sağlamak için kilitleme çok önemli hale gelir.

0

Sorun bireysel operasyonlarla ilgili değil, operasyonların gerçekleştirdiği daha büyük görevlerdir.

Birçok algoritma, üzerinde çalıştıkları durumun tam kontrolü altında oldukları varsayımı ile yazılmıştır. Tanımladığınız gibi sıralı bir sıralı yürütme modeliyle, işlemler birbirleriyle keyfi olarak araya sokulabilir ve durumu paylaşırlarsa, durumun tutarsız bir biçimde olma riski vardır.

Yaptıklarını yapmak için bir değişmezi geçici olarak kırabilecek işlevlerle karşılaştırabilirsiniz. Aracı devlet dışarıdan gözlemlenebilir olmadığı sürece, görevlerine ulaşmak için ne isterse yapabilirler.

Eşzamanlı kod yazarken, ona özel erişiminiz yoksa, belirtilen durumun güvenli olmadığından emin olmanız gerekir. Özel erişim elde etmenin yaygın yolu, kilit tutma gibi bir ilkel senkronizasyonda senkronize etmektir.

Senkronizasyon ilkellerinin bazı platformlarda ortaya çıkma eğiliminde olduğu başka bir şey de, bellekler arası CPU tutarlılığı sağlayan bellek engelleri yaymalarıdır.


0

'Bool' ayarı haricinde, bir değişkenin okunması veya yazılmasının sadece bir komut aldığını (ya da okuma / yazmanın ortasında kesilemeyeceğini) (en azından c olarak) garanti yoktur.


32 bit tam sayı ayarlamak kaç talimat alır?
DXM

1
İlk ifadenizi biraz genişletebilir misiniz? Sadece bir boolun atomik olarak okunabileceğini / yazılabileceğini ima edersiniz, ancak bu mantıklı değildir. "Bool" aslında donanımda mevcut değildir. Genellikle bir bayt veya bir kelime olarak uygulanır, bu nedenle boolbu özelliğe nasıl sahip olabilirsiniz ? Ve bellekten yükleme, değiştirme ve belleğe geri dönme hakkında mı konuşuyorsunuz yoksa kayıt düzeyinde mi bahsediyorsunuz? Kayıtlara tüm okuma / yazma işlemleri kesintisizdir, ancak mem yükü sonra mem deposu değildir (tek başına 2 talimat olduğundan, değeri değiştirmek için en az 1 tane daha).
Corbin

1
Hiper iş parçacıklı / çok çekirdekli / dal tahminli / çok önbellekli bir CPU'da tek bir komut kavramı biraz zor - ancak standart, okuma / yazma işleminin ortasında bir bağlam anahtarına karşı sadece 'bool'un güvenli olması gerektiğini söylüyor tek değişkenli. Bir destek var :: Mutex'i diğer türlerin etrafına saran Atomic ve bence c ++ 11 daha fazla ipucu garantisi
Martin Beckett

Açıklama the standard says that only 'bool' needs to be safe against a context switch in the middle of a read/write of a single variablegerçekten cevaba eklenmelidir.
Wolf

0

Paylaşılan bellek.

Bu ... iş parçacığının tanımı : paylaşımlı belleğe sahip bir dizi eşzamanlı süreç.

Paylaşılan bir bellek yoksa, bunlar genellikle eski okul-UNIX süreçleri olarak adlandırılır .
Bununla birlikte, paylaşılan bir dosyaya erişirken zaman zaman bir kilide ihtiyaçları olabilir.

(UNIX benzeri çekirdeklerde paylaşılan bellek gerçekten de genellikle paylaşılan bellek adresini temsil eden sahte bir dosya tanımlayıcı kullanılarak uygulandı)


0

CPU her seferinde bir komut çalıştırır, ancak iki veya daha fazla CPU'nuz varsa ne olur?

Programı atomik talimatlardan yararlanacak şekilde yazabiliyorsanız, kilitlerin gerekli olmadığı konusunda haklısınız: çalıştırılması verilen işlemcide kesilemeyen ve diğer işlemcilerin müdahalesinden uzak olan talimatlar.

Birkaç talimatın parazitten korunması gerektiğinde kilitler gerekir ve eşdeğer bir atomik talimat yoktur.

Örneğin, çift bağlantılı bir listeye bir düğüm eklemek için birkaç bellek konumunun güncellenmesi gerekir. Eklemeden önce ve eklemeden sonra, bazı değişmezler listenin yapısı hakkında bilgi sahibi olurlar . Ancak, ekleme sırasında, bu değişmezler geçici olarak bozulur: liste "yapım aşamasında" durumundadır.

Değişmezler sırasında başka bir iş parçacığı listede dolaşırsa veya böyle bir durum olduğunda değiştirmeye çalışırsa, veri yapısı muhtemelen bozulur ve davranış tahmin edilemez olur: belki yazılım çökebilir veya yanlış sonuçlarla devam eder. Bu nedenle, iş parçacığı, liste güncellenirken bir şekilde birbirlerinin yolundan uzak durmayı kabul etmelidir.

Uygun şekilde tasarlanmış listeler, atomik talimatlarla manipüle edilebilir, böylece kilitlere ihtiyaç duyulmaz. Bunun için algoritmalara "kilitsiz" denir. Bununla birlikte, atomik talimatların aslında bir kilitleme biçimi olduğunu unutmayın. Özel olarak donanımda uygulanırlar ve işlemciler arasındaki iletişim yoluyla çalışırlar. Atomik olmayan benzer talimatlardan daha pahalıdırlar.

Atomik talimatların lüksüne sahip olmayan çok işlemcilerde, karşılıklı dışlama için ilkellerin basit bellek erişimleri ve seçim döngüleri oluşturması gerekir. Bu tür sorunlar, Edsger Dijkstra ve Leslie Lamport'un benzerleri üzerinde çalışıldı.


Bilginize, sadece tek bir karşılaştırma ve takas kullanarak çift bağlantılı liste güncellemelerini işlemek için kilitsiz algoritmalar okudum. Ayrıca, donanımda, çift karşılaştırma ve takastan (68040'da uygulanmış ancak diğer 68xxx işlemcilerde taşımayan) çok daha ucuz gibi görünecek bir tesis hakkında beyaz bir kitap okudum: yükü uzat bağlı / depoya bağlı iki bağlı yüke ve koşullu depolamaya izin verir, ancak iki depo arasında gerçekleşen bir erişim ilkini geri almaz. Bu, çift karşılaştırma ve mağazadan uygulamak çok daha kolay ...
supercat

... ancak çift bağlantılı liste güncellemelerini yönetmeye çalışırken benzer avantajlar sunar. Anlayabildiğim kadarıyla, çift bağlantılı yük yakalanmadı, ancak herhangi bir talep olsaydı donanım maliyeti oldukça ucuz görünüyordu.
supercat
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.