Değişmezlik, çok işlemcili programlamada kilit ihtiyacını tamamen ortadan kaldırıyor mu?


39

Bölüm 1

Açıkça dokunulmazlık , çok işlemcili programlamada kilit gereksinimini en aza indirir , ancak bu ihtiyacı ortadan kaldırır mı, yoksa değişmezliğin tek başına yeterli olmadığı durumlar var mı? Bana öyle geliyor ki, çoğu program aslında bir şeyler yapmak zorunda kalmadan çok uzun bir süre önce işlemeyi erteleyebiliyor ve kapsülleyebiliyorsunuz (bir veri deposunu güncellemek, rapor üretmek, istisna yapmak vb.). Bu tür işlemler her zaman kilitler olmadan yapılabilir mi? Her bir nesneyi atma ve orijinali değiştirmek yerine yeni bir nesne oluşturma eylemi (değişmezliğin kaba bir görünümü) süreçler arası çekişmelerden mutlak bir koruma sağlıyor mu, yoksa hala kilitlenmesi gereken köşe davaları var mı?

Birçok işlevsel programcı ve matematikçinin “yan etki yok” hakkında konuşmaktan hoşlandığını biliyorum, ancak “gerçek dünyada” her şeyin bir yan etkisi vardır, makine talimatını yerine getirme zamanı olsa bile. Hem teorik / akademik yanıt hem de pratik / gerçek dünya yanıtıyla ilgileniyorum.

Değişmezlik güvenliyse, belirli sınırlar veya varsayımlar göz önüne alındığında, "güvenlik bölgesi" nin sınırlarının tam olarak ne olduğunu bilmek istiyorum. Bazı olası sınırlara örnekler:

  • I / O
  • İstisnalar / hatalar
  • Diğer dillerde yazılmış programlarla etkileşimler
  • Diğer makinelerle etkileşimler (fiziksel, sanal veya teorik)

@JimmaHoffa'ya bu soruyu başlatan yorumu için özel teşekkürler !

Bölüm 2

Çoklu işlemcili programlama genellikle optimizasyon tekniği olarak kullanılır - bazı kodların daha hızlı çalışmasını sağlamak için. Değişmez nesnelere karşı kilit kullanmak ne zaman daha hızlıdır?

Amdahl Yasasında belirtilen sınırlar göz önüne alındığında, değişken nesnelere karşı kilitlenebilir nesnelerle ne zaman daha iyi performans elde edersiniz (çöp toplayıcıyla birlikte veya dikkate alınmadan)?

özet

Bu iki soruyu bir araya getirip, sınırlama kutusunun diş açma sorunlarına bir çözüm olarak geçirgenlik için olduğu yeri bulmaya çalışıyorum.


21
but everything has a side effect- Hayır, değil. Bir değeri kabul eden ve başka bir değer veren ve işlev dışında hiçbir şeyi rahatsız etmeyen bir fonksiyonun hiçbir yan etkisi yoktur ve bu nedenle iş parçacığı için güvenlidir. Bilgisayarın elektrik kullanması önemli değil. İsterseniz bellek hücrelerine de vuran kozmik ışınlardan bahsedebiliriz, isterseniz argümanı pratik tutalım. İşlevin çalışma şeklini güç tüketimini nasıl etkilediğini düşünmek istiyorsanız, bu güvenli bir programlamaya göre farklı bir problemdir.
Robert Harvey

5
@RobertHarvey - Belki de sadece farklı bir yan etki tanımı kullanıyorum ve bunun yerine "gerçek dünya yan etkisi" demeliydim. Evet, matematikçiler yan etkisi olmayan fonksiyonlara sahiptir. Gerçek dünyadaki bir makinede çalışan kod, veri mutasyona uğratıp aktarmadığına bakmak için makine kaynaklarını alır. Örneğinizdeki işlev, çoğu makine mimarisinde geri dönüş değerini yığına yerleştirir.
GlenPeterson

1
Aslında üzerinden alabilirsiniz, ben sorunun yanıtını bu rezil kağıt kalbine gider düşünüyorum research.microsoft.com/en-us/um/people/simonpj/papers/...
Jimmy Hoffa'dan

6
Tartışmamızın amaçları için , uygulama ayrıntılarının alakasız olduğu bir tür iyi tanımlanmış programlama dili yürüten Turing-complete bir makineye atıfta bulunduğunuzu farz ediyorum . Başka bir deyişle, yığının ne yaptığı önemli değil, eğer programlama dilimde yazdığım fonksiyon dilin sınırları içindeki değişmezliği garanti edebilir . Yüksek seviyede bir dilde programlama yaparken yığın hakkında düşünmüyorum, ne de yapmamalıyım.
Robert Harvey

1
@RobertHarvey kaşıkçılık; Monads heh Bunu ilk çift sayfalarından toplayabilirsin. Bahsettiğim için, tüm süreç boyunca yan etkilerin pratik olarak saf bir şekilde ele alınmasına yönelik bir teknik detaylandırıyor, Glen'in sorusuna cevap vereceğinden eminim, bu yüzden bunu bu soruyu bulan kişi için iyi bir not olarak yayınladı. ileri okuma için gelecek.
Jimmy Hoffa

Yanıtlar:


35

Bu, tamamen cevaplandığında gerçekten, gerçekten geniş olan garip bir şekilde ifade edilen bir sorudur. İstediğiniz bazı özellikleri temizlemeye odaklanacağım.

Taklit edilebilirlik bir tasarım takasıdır. Bazı işlemleri zorlaştırır (büyük nesnelerdeki durumu hızlı bir şekilde değiştirmek, nesneleri parçalamak, çalışan bir durumu tutmak, vb.) Başkalarının lehine (kolay hata ayıklama, program davranışı hakkında kolay akıl yürütme, çalışırken sizin için değişen şeyler konusunda endişelenmenize gerek kalmaz) eşzamanlı olarak, vb.) Bu soruya önem verdiğimiz son şey, ancak bunun bir araç olduğunu vurgulamak istiyorum. Çoğunlukla neden olduğundan daha fazla sorun çözen iyi bir araç (çoğu modern programda), ancak gümüş bir mermi değil ... Programların içsel davranışlarını değiştiren bir şey değil.

Şimdi, sana ne kazandırdı? Sabitlenemezlik size bir şey verir: değişmez nesneyi özgürce okuyabilirsiniz, altınızdaki durumu değiştirmekten endişe duymadan (gerçekten gerçekten değişmez olduğu varsayılırsa ... Değişken üyelerle değişmez bir nesneye sahip olmak genellikle bir anlaşma kırıcıdır). Bu kadar. Sizi eşzamanlılığı yönetmek zorunda bırakmaz (kilitler, anlık görüntüler, veri bölümleme veya diğer mekanizmalar; orijinal sorunun kilitler üzerine odaklanması ... Sorunun kapsamı göz önüne alındığında yanlış).

Birçok şey nesne okuyor olsa da ortaya çıkıyor. IO var, ama IO kendisi eşzamanlı kullanımı iyi idare etmiyor. Neredeyse tüm işlemler yapar, ancak diğer nesneler değişken olabilir veya işlemin kendisi eşzamanlılık için uygun olmayan bir durum kullanabilir. Bir nesneyi kopyalamak, bazı dillerde büyük gizli bir sorun noktasıdır; çünkü tam kopya (neredeyse) hiçbir zaman atomik bir işlem değildir. Değişmez nesnelerin size yardımcı olduğu yer burasıdır.

Performans gelince, uygulamanıza bağlıdır. Kilitler (genellikle) ağırdır. Diğer eşzamanlılık yönetimi mekanizmaları daha hızlıdır, ancak tasarımınız üzerinde büyük etkisi vardır. Genel olarak , değişmez nesnelerden yararlanan (ve zayıflıklarını önleyen) yüksek derecede eşzamanlı bir tasarım, değişken nesneleri kilitleyen yüksek derecede eşzamanlı bir tasarımdan daha iyi performans gösterir. Programınız aynı anda hafif ise, o zaman değişir ve / veya önemli değil.

Ancak performans sizin en büyük endişeniz olmamalıdır. Eşzamanlı programlar yazmak zordur . Eşzamanlı programlarda hata ayıklama zordur . Değiştirilebilir nesneler eşzamanlılık yönetimini manuel olarak uygulamada hata yapma fırsatlarını ortadan kaldırarak programınızın kalitesini artırmanıza yardımcı olur. Hata ayıklamayı kolaylaştırır çünkü eşzamanlı bir programdaki durumu izlemeye çalışmazsınız. Tasarımınızı daha basit hale getirir ve böylece orada bulunan hataları giderir.

Özetle: değişmezlik, eşzamanlılığı doğru bir şekilde ele almak için gereken zorlukları ortadan kaldırır ancak ortadan kaldırmaz. Bu yardım yaygın olma eğilimindedir, ancak en büyük kazançlar performanstan ziyade kalite perspektifindendir. Ve hayır, değişmezlik, uygulamanızdaki eşzamanlılığı yönetmekten sizi sihirli bir şekilde mazeret etmez, üzgünüm.


+1 Bu mantıklı, ama derinden değişmez bir dilde eşzamanlılığı düzgün bir şekilde ele alma konusunda hala endişelenmeniz gereken yerlere bir örnek verebilir misiniz? Yaptığını belirtiyorsun, ama böyle bir senaryo bana açık değil
Jimmy Hoffa

@JimmyHoffa Değişmez bir dilde, iş parçacıkları arasındaki durumu güncellemek için hala bir yere ihtiyacınız var. Bildiğim en değişken iki dil (Clojure ve Haskell), dişler arasında değiştirilmiş durumu göndermenin bir yolunu sağlayan bir referans tipi (atomlar ve Mvarlar) sağlar. Ref türlerinin semantiği belirli eşzamanlılık hatalarını önler, ancak diğerleri hala mümkündür.
stonemetal

Stonemetal ilginç, Haskell ile 4 ay içinde henüz Mvars bile duymamıştım, her zaman Erlang'ın mesajını geçerken düşündüm gibi davranan eşzamanlılık durum iletişimi için STM kullandım. Her ne kadar düşünebileceğim eşzamanlı sorunları çözmeme konusundaki mükemmel değişmezlik örneği bir kullanıcı arayüzünü güncellemek olsa da, farklı veri sürümleriyle bir kullanıcı arayüzünü güncellemeye çalışan 2 iş parçacığınız varsa, birinin daha yeni olabilir ve bu nedenle ikinci bir güncelleme alması gerekir. Sıralamayı garanti etmen gereken bir yarış durumu .. İlginç düşünce .. Detaylar için teşekkürler
Jimmy Hoffa

1
@jimmyhoffa - En yaygın örnek IO'dur. Dil değişmez olsa bile, veritabanınız / web siteniz / dosyanız değil. Başka bir tipik harita / küçültmek. İmkansızlık, haritanın toplanmasının daha kusursuz olduğu anlamına gelir, ancak yine de 'tüm harita paralel olarak yapıldıktan sonra koordinasyonu azaltmanız' gerekir.
Telastyn

1
@JimmyHoffa: MVars, diğer dillerde gördüklerinizden çok farklı olmayan, düşük seviyeli değişken eşzamanlılık ilkel (teknik olarak değişken bir depolama konumuna referans). kilitlenmeler ve yarış koşulları çok mümkündür. STM, kilitlenmeyen değiştirilebilir paylaşılan hafıza (mesaj iletmekten çok farklı) için, kilitlenme veya yarış koşulları olmadan hiçbir şekilde birleşim yapılmayan işlemlere izin veren üst düzey bir eşzamanlılık soyutlamasıdır. Değiştirilemez veriler sadece güvenlidir, bunun hakkında söylenecek başka bir şey yoktur.
CA McCann

13

Bir değeri kabul edip başka bir değer döndüren ve işlev dışında hiçbir şeyi rahatsız etmeyen bir fonksiyonun hiçbir yan etkisi yoktur ve bu nedenle iş parçacığı için güvenlidir. İşlevin çalışma şeklini güç tüketimini nasıl etkilediğini düşünmek istiyorsanız, bu farklı bir problemdir.

Uygulama detaylarının alakasız olduğu bir tür iyi tanımlanmış programlama dili yürüten Turing-tamamlanmış bir makineye atıfta bulunduğunuzu farz ediyorum. Başka bir deyişle, yığının ne yaptığı önemli değil, eğer programlama dilimde yazdığım fonksiyon dilin sınırları içindeki değişmezliği garanti edebilir. Yüksek seviyede bir dilde programlama yaparken yığın hakkında düşünmüyorum, ne de yapmamalıyım.

Bunun nasıl çalıştığını göstermek için, C # 'da birkaç basit örnek sunacağım. Bu örneklerin doğru olması için birkaç varsayımda bulunmak zorundayız. Birincisi, derleyici C # belirtimini hatasız takip eder ve ikincisi doğru programları üretir.

Bir dize koleksiyonunu kabul eden basit bir işlev istiyorum ve koleksiyondaki tüm dizelerin virgülle ayrılmış bir dizilimi olan bir dize döndürür diyelim. C # 'daki basit, naif bir uygulama şöyle görünebilir:

public string ConcatenateWithCommas(ImmutableList<string> list)
{
    string result = string.Empty;
    bool isFirst = false;

    foreach (string s in list)
    {
        if (isFirst)
            result += s;
        else
            result += ", " + s;
    }
    return result;
} 

Bu örnek değişmez, prima facie. Bunu nasıl bilebilirim? Çünkü stringnesne değişmez. Ancak, uygulama ideal değildir. Çünkü resultdeğişmez, yeni dize nesne orijinal nesneyi değiştirerek döngü içinde her zaman yaratılmak zorunda resultişaret ettiği. Bu, fazladan telleri temizlemek zorunda kaldığından, hızı olumsuz yönde etkileyebilir ve çöp toplayıcıya baskı uygulayabilir.

Şimdi şunu yapalım diyelim:

public string ConcatenateWithCommas(ImmutableList<string> list)
{
    var result = new StringBuilder();
    bool isFirst = false;

    foreach (string s in list)
    {
        if (isFirst)
            result.Append(s);
        else
            result.Append(", " + s);
    }
    return result.ToString();
} 

Değiştirilebilir string resultbir nesneyle değiştirdiğime dikkat edin StringBuilder. Bu ilk örnekten çok daha hızlıdır, çünkü döngü boyunca her seferinde yeni bir dize oluşturulmaz. Bunun yerine, StringBuilder nesnesi yalnızca her dizeden gelen karakterleri bir karakter koleksiyonuna ekler ve sonunda her şeyi çıkarır.

StringBuilder değişken olmasına rağmen bu işlev değişmez mi?

Evet öyle. Neden? Çünkü her zaman bu fonksiyon denir, yeni bir StringBuilder sadece bu çağrı için oluşturulur. Şimdi iş parçacığı güvenli, ancak değişken bileşenleri içeren saf bir işleve sahibiz.

Ama ya bunu yaparsam?

public class Concatenate
{
    private StringBuilder result = new StringBuilder();
    bool isFirst = false;

    public string ConcatenateWithCommas(ImmutableList<string> list)
    {
        foreach (string s in list)
        {
            if (isFirst)
                result.Append(s);
            else
                result.Append(", " + s);
        }
        return result.ToString();
    } 
}

Bu yöntem iş parçacığı güvenli midir? Hayır değil. Neden? Çünkü sınıf şimdi metodumun dayandığı durumu elinde tutuyor. Artık yöntemde bir yarış durumu var: bir iplik değişebilir IsFirst, ancak başka bir iplik ilkini gerçekleştirebilir, Append()bu durumda dizimin başlangıcında olması gerekmeyen bir virgül var.

Neden böyle yapmak isteyeyim ki? Konuların resultsırasına bakmaksızın ya da konuların girdiği sıraya göre ipleri biriktirmesini isteyebilirim . Belki de bir logger, kim bilir?

Her neyse, düzeltmek için, lockyöntemin doğaçlamalarının etrafına bir ifade koydum .

public class Concatenate
{
    private StringBuilder result = new StringBuilder();
    bool isFirst = false;
    private static object locker = new object();

    public string AppendWithCommas(ImmutableList<string> list)
    {
        lock (locker)
        {
            foreach (string s in list)
            {
                if (isFirst)
                    result.Append(s);
                else
                    result.Append(", " + s);
            }
            return result.ToString();
        }
    } 
}

Şimdi yine iplik güvenli.

Değişmez yöntemlerimin muhtemelen güvenli bir şekilde iş parçacığı için güvenli olamamasının tek yolu, yöntemin bir şekilde uygulamasının bir kısmını sızdırmasıdır. Bu olabilir mi? Derleyici doğru ve program doğru ise. Bu tür yöntemler için kilitlere ihtiyacım olacak mı? Hayır.

Bir eşzamanlılık senaryosunda uygulamanın nasıl sızabileceğine dair bir örnek için, buraya bakın .


2
Hatalı olmadığım sürece, bir Listdeğişken olduğu için , 'saf' olduğunu iddia ettiğiniz ilk işlevde, başka bir iş parçacığı listedeki tüm öğeleri kaldırabilir veya foreach döngüsü içinde bir demet daha ekleyebilir. Bununla oynamak nasıl belli değil IEnumeratorvarlık while(iter.MoveNext())ed ama sürece IEnumerator(şüpheli) değişmez o foreach döngü thrash tehdit olacaktır.
Jimmy Hoffa,

Doğru, iplikler ondan okurken koleksiyonun asla yazılmadığını varsaymanız gerekir. Yöntemi çağıran her iş parçacığı kendi listesini oluşturursa bu geçerli bir varsayım olacaktır.
Robert Harvey,

Referans olarak kullandığı değişken nesneye sahip olduğunda 'saf' diyebileceğini sanmıyorum. Bir IEnumerable aldıysanız olabilir eklemek veya bir IEnumerable'dan öğeleri kaldırmak çünkü bu iddiayı yapabilmek, ama sonra bir Dizi olabilir veya IEnumerable sözleşme herhangi bir şekilde garanti etmez yani Liste IEnumerable olarak teslim saflık Bu işlevi saf hale getirmek için kullanılan asıl teknik, kopyala-kopyala değişmezlik olacaktır, C # bunu yapmaz, bu yüzden işlev onu aldığında Listeyi kopyalamanız gerekir; ama bunu yapmanın tek yolu üzerinde bir foreach ile ...
Jimmy Hoffa

1
@ JimmyHoffa: Kahretsin, beni bu tavuk ve yumurta problemine takıntılı hale getirdin! Herhangi bir yerde bir çözüm görürseniz, lütfen bana bildirin.
Robert Harvey,

1
Şimdi bu cevaba rastladım ve karşılaştığım konuyla ilgili en iyi açıklamalardan biriydi, örnekler süper özlü ve gerçekten çok kolay. Teşekkürler!
Stephen Byrne

4

Sorularınızı anladığımdan emin değilim.

IMHO cevabı evet. Tüm nesnelerinizin değişmez olması durumunda, herhangi bir kilit gerekmez. Ancak bir durumu korumanız gerekiyorsa (örneğin bir veritabanı uygularsanız veya sonuçları birden fazla iş parçacığından toplamanız gerekiyorsa), değişkenliği kullanmanız gerekir; bu nedenle kilitlenir. Taklit edilebilirlik kilitlere olan ihtiyacı ortadan kaldırır, ancak genellikle tamamen değişmez uygulamalara sahip olamazsınız.

2. bölüme verilen cevap - kilitler, kilitsiz konumlardan daima daha yavaş olmalıdır.


3
İkinci bölüm, “Kilitler ve değişken yapılar arasındaki performans değişimi nedir?” Diye soruyor. Cevap bile olsa, muhtemelen kendi sorusunu hak ediyor.
Robert Harvey,

4

Bir grup ilgili durumu, değişmez bir nesneye tek bir değişken referansı içine almak, desen kullanarak birçok durum modifikasyonunun kilitsiz olarak gerçekleştirilmesini mümkün kılabilir:

do
{
   oldState = someObject.State;
   newState = oldState.WithSomeChanges();
} while (Interlocked.CompareExchange(ref someObject.State, newState, oldState) != oldState;

Her iki konu da someObject.stateaynı anda güncelleme yapmaya çalışırsa , her iki nesne de eski durumu okuyacak ve yeni durumun birbirlerinin değişmeden ne olacağını belirleyecektir. CompareExchange'i yürüten ilk iş parçacığı, bir sonraki durumun ne olacağını düşündüğünü saklayacaktır. İkinci iş parçacığı, devletin daha önce okudukları ile uyuşmadığını görecek ve böylece sistemin uygun sonraki durumunu, ilk iş parçacığının değişiklikleri ile yeniden hesaplayacaktır.

Bu şablon, waylaid alan bir dişlinin diğer dişlerin ilerlemesini engellememesi avantajına sahiptir. Diğer bir avantajı, ağır çekişmeler olsa bile, bazı dişlerin her zaman ilerleme kaydetmesidir. Bununla birlikte, dezavantajı vardır; çekişmeli varlıklar halinde, birçok iş parçacığı, atmaları gereken işleri yapmak için çok zaman harcayabilir. Örneğin, ayrı CPU'lardaki 30 iş parçacığının tümü bir nesneyi eşzamanlı olarak değiştirmeye çalışırsa, biri ilk denemesinde, biri ikincisinde, biri üçte birinde vb. verilerini güncellemek için Bir "danışma" kilidinin kullanılması işleri önemli ölçüde iyileştirebilir: bir iş parçacığı güncellemeye çalışmadan önce bir "çekişme" göstergesinin ayarlanıp ayarlanmadığını kontrol etmelidir. Öyleyse, Güncellemeyi yapmadan önce bir kilit edinmesi gerekir. Bir iş parçacığı bir güncellemede birkaç başarısız girişimde bulunursa, rekabet bayrağını ayarlamalıdır. Kilidi almaya çalışan bir iş parçacığı, bekleyen başka kimsenin olmadığını tespit ederse, çekişme bayrağını temizlemelidir. Buradaki kilidin "doğruluk" için gerekli olmadığına dikkat edin; kod onsuz bile düzgün çalışırdı. Kilidin amacı, başarılı olması muhtemel olmayan işlemlere harcanan zaman kodunu en aza indirmektir.


4

İle başla

Açıkça dokunulmazlık, çok işlemcili programlamada kilit ihtiyacını en aza indirir

Yanlış. Kullandığınız her sınıfın belgelerini dikkatlice okumanız gerekir. Örneğin, const std :: string C ++ 'ta iş parçacığı güvenli değil . Değiştirilemeyen nesneler, bunlara erişirken değişen iç durumlara sahip olabilir.

Ancak buna tamamen yanlış bir açıdan bakıyorsunuz. Bir nesnenin değişmez olup olmaması önemli değil, önemli olan onu değiştirip değiştirmemenizdir. Söylediğiniz gibi "hiç bir sürüş sınavına girmezseniz, sarhoş bir sürüş için ehliyetinizi asla kaybedemezsiniz" demektir. Doğru, ama noktayı kaçırmak.

Şimdi örnek kodda, birisi "ConcatenateWithCommas" adlı bir fonksiyonla yazdı: Girdi değişebilirse ve bir kilit kullanırsanız, ne elde edersiniz? Dizeleri birleştirmeye çalışırken bir başkası listeyi değiştirmeye çalışırsa, bir kilit sizi kilitlemekten koruyabilir. Ancak diğer dizileri değiştirmeden önce veya sonra dizgileri birleştirip birleştiremediğinizi hala bilmiyorsunuz. Yani sonucun oldukça işe yaramaz. Kilitlemeyle ilgili olmayan ve kilitlemeyle çözülemeyen bir sorununuz var. Ama eğer değişmez nesneler kullanıyorsanız ve diğer iş parçacığı tüm nesneyi yenisiyle değiştirirse, eski nesneyi kullanıyorsunuz, yeni nesneyi kullanmıyorsunuz, bu nedenle sonucunuz işe yaramaz. Bu problemler hakkında gerçek bir işlevsel seviyede düşünmelisiniz.


2
const std::stringZayıf bir örnek ve biraz da kırmızı ringa balığı. C ++ dizeleri değişkendir ve constyine de değişmezliği garanti edemez. Tek yaptığı, sadece constfonksiyonların çağrılabileceğidir. Bununla birlikte, bu fonksiyonlar hala iç durumu değiştirebilir ve constatılabilir. Son olarak, başka bir dilde aynı sorun vardır: sırf benim referanstır constanlamına gelmez senin referans too. Hayır, gerçekten değişmez bir veri yapısı kullanılmalı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.