Çok iş parçacıklı bir uygulamanın geliştirilmesi. Çok iş parçacıklı uygulamaların mimarileri. Temel senkronizasyon araçları

  • 29.06.2020

Kil Breshear'lar

giriiş

Intel uzmanları tarafından kullanılan çoklu iş parçacığı yöntemleri dört ana aşamadan oluşur: analiz, geliştirme ve uygulama, hata ayıklama ve performans ayarlama. Sıralı program kodundan çok iş parçacıklı bir uygulama oluşturmak için kullanılan bu yaklaşımdır. Birinci, üçüncü ve dördüncü aşamaların uygulanması sırasında yazılımla çalışmak oldukça geniş bir şekilde ele alınırken, ikinci adımın uygulanmasına ilişkin bilgiler açıkça yeterli değildir.

Paralel algoritmalar ve paralel hesaplama üzerine birçok kitap yayınlanmıştır. Ancak, bu yayınlar esas olarak mesaj geçişini, dağıtılmış bellek sistemlerini veya bazen gerçek çok çekirdekli platformlara uygulanamayan teorik paralel hesaplama modellerini kapsar. Çok iş parçacıklı programlamaya ciddi şekilde katılmaya hazırsanız, bu modeller için algoritmaların geliştirilmesi hakkında kesinlikle bilgiye ihtiyacınız olacaktır. Tabii ki, bu modellerin uygulaması oldukça sınırlıdır, bu yüzden birçok yazılım geliştiricisi bunları pratikte uygulamak zorunda kalabilir.

Çok iş parçacıklı uygulamaların geliştirilmesinin öncelikle yaratıcı bir faaliyet ve ancak o zaman bilimsel bir faaliyet olduğu abartısız söylenebilir. Bu makalede, paralel programlama uygulama tabanınızı genişletmenize ve uygulamalarınızda iş parçacığı oluşturma verimliliğini artırmanıza yardımcı olacak sekiz basit kural öğreneceksiniz.

Kural 1. Program kodunda gerçekleştirilen işlemleri birbirinden bağımsız olarak seçin

Paralel işleme, yalnızca birbirinden bağımsız olarak gerçekleştirilen sıralı kod işlemlerine uygulanabilir. Bağımsız eylemlerin gerçek bir tek sonuca nasıl yol açtığına iyi bir örnek, bir evin inşasıdır. Birçok uzmanlık dalındaki işçileri içerir: marangozlar, elektrikçiler, sıvacılar, tesisatçılar, çatı ustaları, ressamlar, duvar ustaları, peyzaj mimarları vb. Elbette bazıları işini bitirmeden çalışmaya başlayamaz (örneğin, çatıcılar duvarlar yapılmadan işe başlamaz, sıva yapılmamışsa ressamlar bu duvarları boyamaya başlamaz). Ama genel olarak inşaatta yer alan tüm kişilerin birbirinden bağımsız hareket ettiğini söyleyebiliriz.

Başka bir örnek ele alalım - belirli filmlerin siparişlerinin geldiği bir DVD kiralama mağazasının çalışma döngüsü. Depoda bu filmleri arayan nokta çalışanları arasında siparişler dağıtılır. Doğal olarak, çalışanlardan biri Audrey Hepburn ile bir filmin kaydedildiği depodan bir disk alırsa, bu Arnold Schwarzenegger ile başka bir aksiyon filmi arayan başka bir çalışanı hiçbir şekilde etkilemeyecek ve hatta daha da fazla etkilemeyecektir. Friends'in yeni sezonu ile disk arayan meslektaşları. Örneğimizde, tüm stokta kalmama sorunlarının, siparişler kiralık dükkana ulaşmadan önce çözüldüğünü ve herhangi bir siparişin paketlenmesi ve nakliyesinin diğerlerinin işlenmesini etkilemeyeceğini varsayıyoruz.

Çalışmanızda, çeşitli yinelemeler veya döngü adımları birbirine bağlı olduğundan ve kesin bir sırayla gerçekleştirilmesi gerektiğinden, paralel olarak değil, yalnızca belirli bir sırayla işlenebilen hesaplamalarla kesinlikle karşılaşacaksınız. Doğadan canlı bir örnek alalım. Hamile bir dişi düşünün. Hamilelik ortalama sekiz ay sürdüğü için, ne derse desin, sekiz geyik aynı anda hamile kalsa bile bir ay içinde bir geyik yavrusu ortaya çıkmaz. Ancak, hepsi Noel Baba'nın kızağına bağlanmış olsaydı, aynı anda sekiz geyik işini mükemmel bir şekilde yapardı.

Kural 2: İnce bir ayrıntı düzeyiyle paralellik kullanın

Sıralı program kodunun paralel olarak ayrılmasına yönelik iki yaklaşım vardır: "aşağıdan yukarıya" ve "yukarıdan aşağıya". İlk olarak, kod analizi aşamasında, program yürütme süresinin önemli bir bölümünü kaplayan kod bölümleri ("sıcak" noktalar olarak adlandırılır) tanımlanır. Bu kod segmentlerini paralel olarak bölmek (mümkünse) maksimum performans kazancı sağlayacaktır.

"Aşağıdan yukarıya" yaklaşım, "sıcak" kod noktalarının çok iş parçacıklı işlenmesini uygular. Bulunan noktaların paralel bölünmesi mümkün değilse, tamamlanması uzun zaman alan paralel bölme için mevcut diğer bölümleri belirlemek için uygulamanın çağrı yığını incelenmelidir. Diyelim ki grafikleri sıkıştıran bir uygulama üzerinde çalışıyorsunuz. Sıkıştırma, tek tek görüntü parçalarını işleyen birkaç bağımsız paralel iş parçacığı kullanılarak uygulanabilir. Bununla birlikte, çok iş parçacıklı sıcak noktalar uygulamayı başarmış olsanız bile, çağrı yığınının analizini ihmal etmeyin, bunun sonucunda daha yüksek bir program kodu düzeyinde bulunan paralel bölme için uygun segmentler bulabilirsiniz. Bu şekilde paralel işlemenin ayrıntı düzeyini artırabilirsiniz.

Yukarıdan aşağıya yaklaşım, program kodunun çalışmasını analiz eder ve yürütülmesi tüm görevin tamamlanmasına yol açan bireysel segmentlerini seçer. Ana kod bölümlerinin belirgin bir bağımsızlığı yoksa, bağımsız hesaplamaları bulmak için bileşenlerini analiz edin. Kodunuzu analiz ederek, yürütülmesi en fazla CPU zamanını alan kod modüllerini belirleyebilirsiniz. Video kodlaması için tasarlanmış bir uygulamada iş parçacığı uygulamasını düşünün. Paralel işleme, diğer gruplardan bağımsız olarak işlenebilen kare grupları için en düşük düzeyde - bir karenin bağımsız pikselleri için veya daha yüksek bir düzeyde - uygulanabilir. Aynı anda birden fazla video dosyasını işlemek için bir uygulama oluşturuluyorsa, bu düzeyde paralel bölme daha da kolay olabilir ve ayrıntı düzeyi en düşük düzeyde olacaktır.

Paralel hesaplamanın ayrıntı düzeyi, iş parçacıkları arasında senkronizasyondan önce gerçekleştirilmesi gereken hesaplama miktarını ifade eder. Başka bir deyişle, senkronizasyon ne kadar az sıklıkla gerçekleşirse, ayrıntı düzeyi o kadar düşük olur. İnce taneli iş parçacıklı hesaplamalar, iş parçacıklarının düzenlenmesiyle ilişkili sistem ek yükünün, bu iş parçacıkları tarafından gerçekleştirilen yararlı hesaplama miktarını aşmasına neden olabilir. Aynı miktarda hesaplamaya sahip iş parçacığı sayısındaki artış, işleme sürecini karmaşıklaştırır. İnce taneli çoklu iş parçacığı, daha az sistem gecikmesine neden olur ve ek iş parçacıklarıyla elde edilebilecek daha fazla ölçeklendirme potansiyeline sahiptir. İnce taneli paralel işleme uygulamak için yukarıdan aşağıya bir yaklaşım kullanılması ve çağrı yığınının yüksek bir düzeyinde iş parçacıklarının düzenlenmesi önerilir.

Kural 3: Çekirdek sayısı arttıkça performansının artması için ölçeklenebilirliği kodunuza dahil edin.

Çok uzun zaman önce, piyasada çift çekirdekli işlemcilere ek olarak dört çekirdekli işlemciler ortaya çıktı. Ayrıca Intel, saniyede bir trilyon kayan nokta işlemi gerçekleştirebilen 80 çekirdekli bir işlemci oluşturduğunu zaten duyurdu. İşlemcilerdeki çekirdek sayısı yalnızca zamanla artacağından, kodunuz ölçeklenebilirlik için uygun potansiyele sahip olmalıdır. Ölçeklenebilirlik, bir uygulamanın sistem kaynaklarındaki artış (çekirdek sayısı, bellek boyutu, veri yolu frekansı vb.) veya veri hacmindeki artış gibi değişikliklere yeterince yanıt verme yeteneğinin yargılanabileceği bir parametredir. Geleceğin işlemcilerinde çekirdek sayısının artacağını göz önünde bulundurarak, sistem kaynaklarının artması nedeniyle performansı artacak ölçeklenebilir kodlar oluşturun.

C. Northecote Parkinson yasalarından birini açıklamak gerekirse, "veri işlemenin mevcut tüm sistem kaynaklarını kapladığını" söyleyebiliriz. Bu, bilgi işlem kaynakları arttıkça (örneğin çekirdek sayısı) hepsinin veri işleme için kullanılacağı anlamına gelir. Yukarıda tartışılan video sıkıştırma uygulamasına geri dönelim. İşlemcideki ek çekirdeklerin görünümünün işlenen çerçevelerin boyutunu etkilemesi pek olası değildir - bunun yerine çerçeveyi işleyen iş parçacıklarının sayısı artacak ve bu da akış başına piksel sayısında azalmaya yol açacaktır. Sonuç olarak, ek iş parçacıklarının organizasyonu nedeniyle, hizmet verilerinin miktarı artacak ve paralellik ayrıntı derecesi azalacaktır. Daha olası başka bir senaryo, kodlanması gereken video dosyalarının boyutunda veya sayısında bir artış olabilir. Bu durumda, daha hacimli (veya ek) video dosyalarını işleyecek ek akışların düzenlenmesi, tüm iş miktarının doğrudan artışın gerçekleştiği aşamada bölünmesine izin verecektir. Buna karşılık, bu tür yeteneklere sahip bir uygulama, ölçeklenebilirlik için yüksek bir potansiyele sahip olacaktır.

Veri ayrıştırmayı kullanarak paralel işlemeyi tasarlamak ve uygulamak, işlevsel ayrıştırmayı kullanmaya kıyasla daha fazla ölçeklenebilirlik sağlar. Program kodundaki bağımsız işlevlerin sayısı genellikle sınırlıdır ve uygulamanın yürütülmesi sırasında değişmez. Her bağımsız işleve ayrı bir iş parçacığı (ve buna bağlı olarak bir işlemci çekirdeği) tahsis edildiğinden, çekirdek sayısındaki artışla birlikte, ek olarak düzenlenmiş iş parçacıkları performans artışına neden olmaz. Bu nedenle, veri ayrıştırma ile paralel bölümleme modelleri, işlemci çekirdeği sayısındaki artışla birlikte işlenen veri miktarının artması nedeniyle uygulama ölçeklenebilirliği için artan potansiyel sağlayacaktır.

Program kodu, bağımsız fonksiyonların iş parçacıklı işlenmesi olsa bile, giriş yükü arttığında başlatılan ek iş parçacıklarının kullanılması mümkündür. Yukarıda tartışılan bir ev inşa etme örneğine geri dönelim. İnşaatın kendine özgü amacı, sınırlı sayıda bağımsız görevi tamamlamaktır. Ancak, iki kat daha fazla kat inşa etmeniz emredildiyse, muhtemelen bazı uzmanlık alanlarında (boyacılar, çatıcılar, tesisatçılar, vb.) ek işçi kiralamak isteyeceksiniz. Bu nedenle, artan yükten kaynaklanan veri ayrışmasına uyum sağlayabilecek uygulamalar tasarlamanız gerekir. Kodunuz işlevsel ayrıştırma uyguluyorsa, işlemci çekirdeği sayısı arttıkça ek iş parçacıkları düzenlemeyi düşünün.

Kural 4: İş parçacığı için güvenli kitaplıklar kullanın

Kod etkin noktalarındaki verileri işlemek için bir kitaplığa ihtiyacınız varsa, kendi kodunuz yerine hazır işlevleri kullanmayı düşünün. Kısacası, işlevleri kütüphanelerden optimize edilmiş prosedürlerde zaten sağlanan kod bölümleri geliştirerek tekerleği yeniden icat etmeye çalışmayın. Intel® Math Kernel Library (Intel® MKL) ve Intel® Integrated Performance Primitives (Intel® IPP) dahil olmak üzere birçok kitaplık, halihazırda çok çekirdekli işlemciler için optimize edilmiş çok kanallı işlevler içermektedir.

Çok iş parçacıklı kitaplıklardan prosedürleri kullanırken, bir veya başka bir kitaplığı çağırmanın iş parçacıklarının normal çalışmasını etkilemeyeceğinden emin olmanız gerektiğini belirtmekte fayda var. Yani, prosedür çağrıları iki farklı iş parçacığından yapılıyorsa, her çağrının doğru sonuçları döndürmesi gerekir. Prosedürler paylaşılan kütüphane değişkenlerine erişir ve bunları güncellerse, hesaplama sonuçlarının güvenilirliğini olumsuz yönde etkileyecek bir "veri yarışı" meydana gelebilir. İş parçacıklarıyla doğru şekilde çalışmak için, kitaplık prosedürü yeni olarak eklenir (yani, yerel değişkenler dışında hiçbir şeyi güncellemez) veya paylaşılan kaynaklara erişimi korumak için senkronize edilir. Sonuç: Program kodunuzda herhangi bir üçüncü taraf kitaplığı kullanmadan önce, iş parçacıklarıyla doğru çalıştığından emin olmak için ekli belgeleri okuyun.

Kural 5: Uygun Bir Diş Açma Modeli Kullanın

Tüm uygun kod bölümlerinin paralel bölünmesi için, çok iş parçacıklı kitaplıkların bileşiminden gelen işlevlerin açıkça yeterli olmadığını ve iş parçacıklarını düzenlemeyi düşünmeniz gerektiğini varsayalım. OpenMP kitaplığı ihtiyacınız olan tüm işlevleri zaten içeriyorsa, kendi (kullanışsız) iş parçacığı yapınızı oluşturmak için acele etmeyin.

Açık çoklu iş parçacığının dezavantajı, kesin iş parçacığı kontrolünün imkansızlığıdır.

İhtiyacınız olan tek şey, kaynak yoğun döngülerin paralel bir şekilde ayrılmasıysa veya açık iş parçacıklarının sağladığı ek esneklik sizin için arka plandaysa, bu durumda fazladan iş yapmanın bir anlamı yoktur. Çoklu iş parçacığının uygulanması ne kadar karmaşıksa, koddaki hata olasılığı o kadar yüksek ve sonraki iyileştirme o kadar zor.

OpenMP kitaplığı, veri ayrıştırmaya odaklanmıştır ve özellikle büyük miktarda bilgi ile çalışan döngüleri işlemek için çok uygundur. Bazı uygulamalar için yalnızca veri ayrıştırmasının geçerli olmasına rağmen, OpenMP kullanımının kabul edilemez olduğu ve açık yöntemler kullanarak çoklu iş parçacığı uygulamaya devam ettiği ek gereksinimleri (örneğin, işveren veya müşteri) dikkate almak gerekir. Böyle bir durumda, OpenMP, olası performans kazancını, ölçeklenebilirliği ve daha sonra açık çoklu iş parçacığı kullanarak kodu bölmek için gerekli olacak yaklaşık çabayı tahmin etmek için ön iş parçacığı oluşturma için kullanılabilir.

Kural 6. Program kodunun sonucu, paralel iş parçacıklarının yürütme sırasına bağlı olmamalıdır.

Sıralı kod için, herhangi bir başka ifadeden sonra yürütülecek bir ifadeyi tanımlamanız yeterlidir. Çok iş parçacıklı kodda, iş parçacıklarının yürütülme sırası tanımlanmamıştır ve işletim sistemi zamanlayıcısının talimatlarına bağlıdır. Kesin konuşmak gerekirse, bazı işlemleri gerçekleştirmek için başlatılan iş parçacıklarının sırasını tahmin etmek veya daha sonraki bir anda zamanlayıcı tarafından hangi iş parçacığının başlatılacağını belirlemek pratik olarak imkansızdır. Tahmin, özellikle organize iş parçacıklarından daha az çekirdeğe sahip bir işlemciye sahip bir platformda çalışırken uygulama gecikmesini azaltmak için kullanılır. Bir iş parçacığı, önbelleğe yazılmayan bir alana erişmesi gerektiğinden veya bir G/Ç isteğini tamamlaması gerektiğinden engellenirse, zamanlayıcı onu duraklatacak ve çalışmaya hazır bir iş parçacığı başlatacaktır.

İş parçacığı planlamasındaki belirsizliğin doğrudan bir sonucu, veri yarışı durumlarıdır. Başka bir iş parçacığı değeri okumadan önce bir iş parçacığının paylaşılan bir değişkenin değerini değiştireceği varsayımı yanlış olabilir. Şans eseri, platforma özgü iş parçacıklarının yürütme sırası, tüm uygulama başlatmalarında aynı kalacaktır. Bununla birlikte, sistem durumundaki en küçük değişiklikler (verinin sabit sürücüdeki konumu, bellek hızı veya hatta AC güç kaynağının nominal frekansından sapma gibi) farklı bir iş parçacığı yürütme sırasına neden olabilir. Bu nedenle, yalnızca belirli bir iş parçacığı dizisiyle doğru şekilde çalışan kod için, veri yarışı durumları ve kilitlenmelerle ilgili sorunlar olasıdır.

Performans açısından, iş parçacıklarının yürütme sırasını kısıtlamamak tercih edilir. Kesin bir dizi yürütme dizisine yalnızca önceden belirlenmiş bir kriter tarafından belirlenen acil durumlarda izin verilir. Bu gibi durumlarda, iş parçacıkları sağlanan senkronizasyon mekanizmaları tarafından belirtilen sırada çalışacaktır. Örneğin, bir masanın üzerine yayılmış bir gazeteyi okuyan iki arkadaşı düşünelim. Birincisi, farklı hızlarda okuyabilirler ve ikincisi, farklı makaleleri okuyabilirler. Ve gazetenin yayılmasını ilk kimin okuduğu önemli değil - her durumda, sayfayı çevirmeden önce arkadaşını beklemek zorunda kalacak. Aynı zamanda, makale okuma zamanı ve sırası konusunda herhangi bir kısıtlama yoktur - arkadaşlar herhangi bir hızda okur ve sayfa açıldığında aralarında hemen senkronizasyon gerçekleşir.

Kural 7. Akışların yerel depolamasını kullanın. Kilitleri gerektiği gibi tek tek veri alanlarına atayın

Senkronizasyon kaçınılmaz olarak sistem üzerindeki yükü arttırır, bu da paralel hesaplamaların sonuçlarını elde etme sürecini hiçbir şekilde hızlandırmaz, ancak doğruluğunu sağlar. Evet, senkronizasyon gereklidir, ancak kötüye kullanılmamalıdır. Senkronizasyonu en aza indirmek için, iş parçacıklarının veya tahsis edilen bellek alanlarının yerel depolanması (örneğin, karşılık gelen iş parçacıklarının tanımlayıcılarıyla etiketlenmiş dizi öğeleri) kullanılır.

Farklı iş parçacıkları arasında geçici değişkenleri paylaşma ihtiyacı nadirdir. Bu tür değişkenler, her iş parçacığına yerel olarak bildirilmeli veya tahsis edilmelidir. Değerleri, iş parçacıklarının yürütülmesinin ara sonuçları olan değişkenler de ilgili iş parçacıklarına yerel olarak bildirilmelidir. Bu ara sonuçları bazı ortak bellek alanlarında özetlemek için senkronizasyon gerekli olacaktır. Sistem üzerindeki olası yükü en aza indirmek için bu ortak alanın mümkün olduğunca seyrek olarak güncellenmesi tercih edilir. Açık çoklu iş parçacığı yöntemleri, çok iş parçacıklı bir kod bölümünün yürütülmesinin başlangıcından sonraki bölümün başlangıcına kadar (veya çok iş parçacıklı bir işleve yapılan bir çağrının işlenmesi sırasında, bir sonraki yürütme işlemine kadar) yerel verilerin bütünlüğünü sağlayan iş parçacığı yerel depolama API'leri sağlar. aynı işlev).

İş parçacıklarının yerel olarak depolanması mümkün değilse, paylaşılan kaynaklara erişim, kilitler gibi çeşitli nesneler kullanılarak senkronize edilir. Kilitlerin sayısı veri bloklarının sayısına eşitse yapılması en kolay olan, belirli veri bloklarına kilitleri doğru şekilde atamak önemlidir. Belleğin birden çok alanına erişimi senkronize eden tek bir kilitleme mekanizması, yalnızca bu alanların tümü kodun aynı kritik bölümünde yer aldığında geçerlidir.

Büyük miktarda veriye, örneğin 10.000 öğeden oluşan bir diziye erişimi senkronize etme ihtiyacı varsa ne yapmalı? Tüm dizi için tek bir kilit düzenlemek, uygulamada kesinlikle bir darboğaz yaratmak anlamına gelir. Gerçekten her öğe için ayrı ayrı engelleme düzenlemek gerekli mi? Daha sonra, verilere 32 veya 64 paralel iş parçacığı erişse bile, oldukça geniş bir bellek alanına erişim çakışmalarını önlemeniz gerekecek ve bu tür çakışmaların olasılığı %1'dir. Neyse ki, "modulo kilitleri" olarak adlandırılan bir tür altın ortalama var. N modulo kilidi kullanılırsa, bunların her biri toplam veri alanının N. kısmına erişimi senkronize eder. Örneğin, bu tür iki kilit düzenlenirse, bunlardan biri çift dizi öğelerine erişimi, ikincisi tek dizi öğelerine erişimi engeller. Bu durumda, gerekli elemana erişen iş parçacıkları paritesini belirler ve uygun kilidi ayarlar. Modulo kilitlerinin sayısı, iş parçacığı sayısı ve birkaç iş parçacığının aynı bellek alanına aynı anda erişme olasılığı dikkate alınarak seçilir.

Birkaç kilitleme mekanizmasının aynı anda kullanılmasının, aynı bellek alanına erişimi senkronize etmesine izin verilmediğini unutmayın. Segal yasasını hatırlayın: “Bir saati olan kişi, saatin kaç olduğunu kesin olarak bilir. Birkaç saati olan bir insan hiçbir şeyden emin değildir. Bir değişkene erişimin iki farklı kilit tarafından kontrol edildiğini varsayalım. Bu durumda, ilk kilit bir kod bölümü tarafından ve ikincisi başka bir bölüm tarafından alınabilir. Daha sonra bu segmentleri yürüten iş parçacıkları, eş zamanlı olarak eriştikleri paylaşılan veriler için kendilerini bir yarış durumunda bulacaktır.

Kural 8: Çoklu iş parçacığı uygulamak için gerekirse programlama algoritmasını değiştirin

Hem seri hem de paralel uygulamaların performansını değerlendirme kriteri yürütme süresidir. Asimptotik sıra, algoritmanın bir tahmini olarak uygundur. Bu teorik gösterge ile uygulamanın performansını neredeyse her zaman değerlendirebilirsiniz. Yani, diğer tüm şeyler eşit olduğunda, O(n log n) büyüme hızına (hızlı sıralama) sahip bir uygulama, bu uygulamaların sonuçları aynı olsa bile, O(n2) büyüme hızı uygulamasından (seçici sıralama) daha hızlı performans gösterecektir. aynı.

Asimptotik yürütme sırası ne kadar iyi olursa, paralel uygulama o kadar hızlı çalışır. Ancak, en verimli sıralı algoritma bile her zaman paralel iş parçacıklarına bölünemez. Bir program etkin noktasının bölünmesi çok zorsa ve bu etkin noktanın çağrı yığınının daha yüksek bir düzeyinde çoklu iş parçacığı oluşturmanın bir yolu yoksa, ilk olarak, bölünmesi orijinal olandan daha kolay olan farklı bir sıralı algoritma kullanmayı düşünmelisiniz. Elbette, iş parçacığı işleme için kod hazırlamanın başka yolları da vardır.

Son ifadenin bir örneği olarak, iki kare matrisin çarpımını düşünün. Strassen'in algoritması en iyi asimptotik yürütme emirlerinden birine sahiptir: O(n2.81), geleneksel üçlü iç içe döngü algoritmasının O(n3) sıralamasından çok daha iyidir. Strassen'in algoritmasına göre, her matris dört alt matrise bölünür, ardından n/2 × n/2 alt matrisleri çarpmak için yedi özyinelemeli çağrı yapılır. Özyinelemeli çağrıları paralelleştirmek için, belirli bir boyuta ulaşana kadar yedi bağımsız alt matris çarpımını sırayla gerçekleştirecek yeni bir iş parçacığı oluşturabilirsiniz. Bu durumda, iş parçacığı sayısı katlanarak artacak ve alt matrislerin boyutunda bir azalma ile yeni oluşturulan her bir iş parçacığı tarafından yapılan hesaplamaların detay derecesi artacaktır. Başka bir seçenek düşünün - aynı anda çalışan ve bir alt matris çarpımı gerçekleştiren yedi iş parçacığından oluşan bir havuz düzenlemek. İş parçacığı havuzunun tamamlanmasının ardından, alt matris çarpımı için Strassen yöntemine özyinelemeli bir çağrı gerçekleşir (program kodunun sıralı sürümünde olduğu gibi). Böyle bir programı çalıştıran sistemde sekizden fazla işlemci çekirdeği varsa, bazıları boşta olacaktır.

Matris çarpma algoritması, üçlü iç içe döngü kullanarak paralel bölmeye tabi tutmak çok daha kolaydır. Bu durumda, matrislerin satırlara, sütunlara veya alt matrislere bölündüğü ve akışların her birinin belirli hesaplamalar yaptığı veri ayrıştırma kullanılır. Böyle bir algoritmanın uygulanması, döngünün bazı seviyelerine eklenen OpenMP pragmaları kullanılarak veya matris bölümü gerçekleştiren iş parçacıklarının açıkça düzenlenmesiyle gerçekleştirilir. Bu daha basit sıralı algoritmayı uygulamak için, çok iş parçacıklı Strassen algoritmasının uygulanmasına kıyasla çok daha az kod değişikliği gerekecektir.

Artık seri kodu etkili bir şekilde paralele dönüştürmek için sekiz basit kural biliyorsunuz. Bu kuralları izleyerek, gelişmiş güvenilirlik, optimum performans ve daha az darboğaz sunan çok iş parçacıklı çözümleri hızla oluşturacaksınız.

Çok İş parçacıklı Programlama Öğreticileri web sayfasına dönmek için şuraya gidin:

E Bu makale, bu yılan karmaşasını çözmenin çocuk oyuncağı olduğunu düşünen deneyimli Python terbiyecileri için değil, yeni piton bağımlıları için çoklu kullanım olanaklarına yüzeysel bir genel bakış.

Ne yazık ki, Python'da çoklu okuma konusunda Rusça'da çok fazla materyal yok ve örneğin GIL hakkında hiçbir şey duymamış pythoners kıskanılacak bir düzenlilikle karşıma çıkmaya başladı. Bu yazıda çok iş parçacıklı bir python'un en temel özelliklerini açıklamaya çalışacağım, size GIL'in ne olduğunu ve onunla (veya onsuz) nasıl yaşayacağınızı ve çok daha fazlasını anlatacağım.


Python büyüleyici bir programlama dilidir. Birçok programlama paradigmasını mükemmel bir şekilde birleştirir. Bir programcının karşılaşabileceği görevlerin çoğu burada kolay, zarif ve özlü bir şekilde çözülür. Ancak tüm bu görevler için genellikle tek iş parçacıklı bir çözüm yeterlidir ve tek iş parçacıklı programlar genellikle tahmin edilebilir ve hata ayıklaması kolaydır. Çok iş parçacıklı ve çok işlemli programlar hakkında söylenemez.

Çok iş parçacıklı Uygulamalar


Python'un bir modülü var diş açma , ve çok iş parçacıklı programlama için ihtiyacınız olan her şeye sahiptir: çeşitli kilit türleri, bir semafor ve bir olay mekanizması vardır. Tek kelime - çok iş parçacıklı programların büyük çoğunluğu için gerekli olan her şey. Üstelik tüm bu araçları kullanmak oldukça basit. 2 iş parçacığı başlatan örnek bir program düşünün. Bir iş parçacığı on "0" yazar, diğeri - on "1" ve kesinlikle sırayla.

içe aktarma iş parçacığı

kesin yazar

xrange(10 ) içindeki i için:

x yazdır

Event_for_set.set()

# init olayları

e1 = iş parçacığı oluşturma.Event()

e2 = iş parçacığı oluşturma.Event()

# init iş parçacığı

0 , e1, e2))

1 , e2, e1))

# konu başlat

t1.başlangıç()

t2.başlangıç()

t1.join()

t2.join()


Sihir yok, vudu kodu yok. Kod açık ve tutarlıdır. Ayrıca gördüğünüz gibi bir fonksiyondan thread oluşturduk. Küçük görevler için bu çok uygundur. Bu kod da oldukça esnektir. "2" yazan bir 3. işlemimiz olduğunu varsayalım, kod şöyle görünecektir:

içe aktarma iş parçacığı

kesin yazar (x, event_for_wait, event_for_set):

xrange(10 ) içindeki i için:

Event_for_wait.wait() # olay için bekle

Event_for_wait.clear() # gelecek için temiz olay

x yazdır

Event_for_set.set() # komşu iş parçacığı için olay ayarla

# init olayları

e1 = iş parçacığı oluşturma.Event()

e2 = iş parçacığı oluşturma.Event()

e3 = iş parçacığı oluşturma.Event()

# init iş parçacığı

t1 = threading.Thread(hedef=yazar, args=( 0 , e1, e2))

t2 = threading.Thread(hedef=yazar, args=( 1 , e2, e3))

t3 = threading.Thread(hedef=yazar, args=( 2 , e3, e1))

# konu başlat

t1.başlangıç()

t2.başlangıç()

t3.başlangıç()

e1.set() # ilk olayı başlat

# konuları ana konuya birleştir

t1.join()

t2.join()

t3.join()


Yeni bir olay, yeni bir konu ekledik ve hangi parametrelerle biraz değiştirdik?
ileti dizileri başlar (elbette, örneğin MapReduce kullanarak daha genel bir çözüm yazabilirsiniz, ancak bu zaten bu makalenin kapsamı dışındadır).
Gördüğünüz gibi, hala bir sihir yok. Her şey basit ve net. Daha ileri gidelim.

Küresel Tercüman Kilidi


İş parçacığı kullanmanın en yaygın iki nedeni vardır: birincisi, modern işlemcilerin çok çekirdekli mimarisini kullanma verimliliğini ve dolayısıyla programın performansını artırmak;
ikincisi, programın mantığını paralel tam veya kısmen asenkron bölümlere ayırmamız gerekirse (örneğin, aynı anda birkaç sunucuya ping yapabilmek için).

İlk durumda, Global Tercüman Kilidi (veya kısaca GIL) gibi bir Python (veya daha doğrusu ana uygulaması CPython) sınırlamasıyla karşı karşıyayız. GIL kavramı, bir işlemcide aynı anda yalnızca bir iş parçacığının yürütülebilmesidir. Bu, bireysel değişkenler için iş parçacıkları arasında bir mücadele olmaması için yapılır. Yürütülebilir iş parçacığının tüm ortama erişimi vardır. Python'da iş parçacığı uygulamasının bu özelliği, iş parçacığıyla çalışmayı büyük ölçüde basitleştirir ve belirli bir iş parçacığı güvenliği (iplik güvenliği) sağlar.

Ancak burada ince bir nokta var: Çok iş parçacıklı bir uygulama, aynı şeyi yapan tek iş parçacıklı bir uygulama ile tam olarak aynı miktarda veya CPU'daki her bir iş parçacığının yürütme süresinin toplamı için çalışacak gibi görünebilir. . Ama burada hoş olmayan bir etki bekliyoruz. Programı düşünün:

fout olarak open("test1.txt" , "w" ) ile:

xrange(1000000 ) içindeki i için:

yazdır >> fout, 1


Bu program bir dosyaya bir milyon "1" satır yazar ve bunu bilgisayarımda ~0.35 saniyede yapar.

Başka bir program düşünün:

iş parçacığı içe aktarma iş parçacığından

def yazar(dosya adı, n):

fout olarak open(filename, "w" ) ile:

xrange(n) içindeki i için:

yazdır >> fout, 1

t1 = Konu(hedef=yazar, argümanlar=("test2.txt" , 500000 ,))

t2 = Thread(hedef=yazar, args=("test3.txt" , 500000 ,))

t1.başlangıç()

t2.başlangıç()

t1.join()

t2.join()


Bu program 2 iş parçacığı oluşturur. Her akışta ayrı bir dosyaya yarım milyon “1” satır yazar. Aslında, iş miktarı önceki programla aynıdır. Ancak zamanla burada ilginç bir etki elde edilir. Program 0,7 saniyeden 7 saniyeye kadar çalışabilir. Bu neden oluyor?

Bunun nedeni, bir iş parçacığının CPU kaynağına ihtiyacı olmadığında, GIL'yi serbest bırakması ve o anda onu, başka bir iş parçacığını ve ayrıca ana iş parçacığını almaya çalışabilmesidir. Aynı zamanda çok sayıda çekirdek olduğunu bilen işletim sistemi, çekirdekler arasında iş parçacığı dağıtmaya çalışarak her şeyi ağırlaştırabilir.

UPD: Şu anda, Python 3.2'de, özellikle her bir iş parçacığının kontrolü kaybettikten sonra kısa bir süre beklemesi nedeniyle, bu sorunun kısmen çözüldüğü gelişmiş bir GIL uygulaması var. GIL'i tekrar yakalayabilir (ingilizcede iyi sunumlar var

"Python'da verimli, çok iş parçacıklı programlar yazamıyor musunuz?" diye soruyorsunuz. Hayır, elbette, bir çıkış yolu ve hatta birkaç tane var.

Çok İşlemli Uygulamalar


Önceki paragrafta açıklanan sorunu bir şekilde çözmek için Python'un bir modülü vardır. alt süreç . Paralel bir iş parçacığında yürütmek istediğimiz bir program yazabiliriz (aslında zaten bir süreç). Ve başka bir programda bir veya daha fazla iş parçacığında çalıştırın. Bu yol, programımızı gerçekten hızlandıracaktır, çünkü GIL başlatıcısında oluşturulan iş parçacıkları açılmaz, yalnızca çalışan işlemin tamamlanmasını bekler. Ancak bu yöntemle ilgili birçok sorun var. Asıl sorun, süreçler arasında veri aktarımının zorlaşmasıdır. Nesneleri bir şekilde seri hale getirmek, PIPE veya diğer araçlar aracılığıyla iletişim kurmak gerekli olacaktır, ancak tüm bunlar kaçınılmaz olarak ek maliyetler getirir ve kodun anlaşılması zorlaşır.

Burada farklı bir yaklaşım kullanabiliriz. Python'un çok işlemli bir modülü var . İşlevsel olarak, bu modül benzer diş açma . Örneğin, süreçler sıradan işlevlerden aynı şekilde oluşturulabilir. İşlemlerle çalışma yöntemleri, iş parçacığı modülündeki iş parçacıklarıyla neredeyse aynıdır. Ancak süreçleri senkronize etmek ve veri alışverişi yapmak için diğer araçları kullanmak gelenekseldir. Kuyruklardan (Queue) ve kanallardan (Pipe) bahsediyoruz. Bununla birlikte, iş parçacığında olan kilitlerin, olayların ve semaforların analogları da vardır.

Ek olarak, çoklu işlem modülü, paylaşılan bellekle çalışmak için bir mekanizmaya sahiptir. Bunu yapmak için modül, süreçler arasında “genelleştirilebilen” (paylaşılabilen) değişken (Değer) ve dizi (Array) sınıflarına sahiptir. Paylaşılan değişkenlerle çalışmanın rahatlığı için yönetici sınıflarını (Manager) kullanabilirsiniz. Daha esnek ve kullanımı kolaydır, ancak daha yavaştırlar. Multiprocessing.sharedctypes modülünü kullanarak ctypes modülünden türleri paylaşmanın güzel yeteneğinden bahsetmiyorum bile.

Çoklu işlem modülü ayrıca işlem havuzları oluşturmak için bir mekanizmaya sahiptir. Bu mekanizma, Usta-İşçi modelini uygulamak veya paralel bir Haritayı (bir anlamda Usta-İşçinin özel bir durumu olan) uygulamak için çok kullanışlıdır.

Çoklu işlem modülüyle çalışmanın ana sorunlarından, bu modülün göreceli platform bağımlılığına dikkat etmek önemlidir. İşlemlerle çalışma farklı işletim sistemlerinde farklı düzenlendiğinden, koda bazı kısıtlamalar getirilir. Örneğin, Windows'ta çatal mekanizması yoktur, bu nedenle işlem ayırma noktası şu şekilde sarılmalıdır:

if __name__ == "__main__" :


Ancak, bu tasarım zaten iyi bir formdur.

Başka...


Python'da paralel uygulamalar yazmak için başka kütüphaneler ve yaklaşımlar da vardır. Örneğin, Python'da (pyMPI, mpi4py) Hadoop+Python veya MPI'nin çeşitli uygulamalarını kullanabilirsiniz. Mevcut C++ veya Fortran kitaplıkları için sarmalayıcıları bile kullanabilirsiniz. Burada Pyro, Twisted, Tornado ve diğerleri gibi çerçevelerden/kütüphanelerden bahsetmek mümkündü. Ancak bunların hepsi bu makalenin kapsamı dışındadır.

Tarzımı beğendiyseniz, bir sonraki yazıda size PLY'de basit tercümanların nasıl yazılacağını anlatmaya çalışacağım. ve ne için kullanılabilirler.

Çok iş parçacıklı programlama, olaya dayalı grafik kullanıcı arabirimleri yazmaktan ve hatta basit sıralı uygulamalar oluşturmaktan temel olarak farklı değildir. Kapsülleme, endişelerin ayrılması, gevşek bağlantı vb. ile ilgili tüm önemli kurallar burada geçerlidir. Ancak birçok geliştirici, tam olarak bu kuralları göz ardı ettikleri için çok iş parçacıklı programlar yazmayı zor buluyor. Bunun yerine, yeni başlayanlar için çok iş parçacıklı programlama metinlerinden derlenen evreler ve senkronizasyon ilkelleri hakkında çok daha az önemli bilgileri uygulamaya koymaya çalışıyorlar.

Peki nedir bu kurallar

Bir sorunla karşılaşan başka bir programcı, "Ah, doğru, normal ifadeler kullanmamız gerekiyor" diye düşünüyor. Ve şimdi iki sorunu var - Jamie Zawinski.

Bir sorunla karşılaşan başka bir programcı şöyle düşünüyor: "Ah, doğru, burada akışları kullanacağım." Ve şimdi on sorunu var - Bill Schindler.

Goethe'nin baladının kahramanı gibi, çok iş parçacıklı kod yazmayı taahhüt eden çok fazla programcının başı belaya giriyor " Büyücünün Çırağı". Programcı, prensipte çalışan bir dizi iş parçacığının nasıl oluşturulacağını öğrenecektir, ancak er ya da geç kontrolden çıkarlar ve programcı ne yapacağını bilemez.

Ancak yarı eğitimli sihirbazın aksine, talihsiz programcı, sihirli değneğini sallayacak ve düzeni yeniden kuracak güçlü bir büyücünün gelişini umut edemez. Bunun yerine, programcı sürekli ortaya çıkan sorunlarla başa çıkmaya çalışarak en çirkin numaralara gider. Sonuç her zaman aynıdır: aşırı karmaşık, sınırlı, kırılgan ve güvenilmez bir uygulama. Kötü çok iş parçacıklı kodun doğasında var olan kalıcı bir kilitlenme tehdidi ve diğer tehlikeler vardır. Açıklanamayan çökmelerden, düşük performanstan, eksik veya yanlış sonuçlardan bahsetmiyorum.

Merak ediyor olabilirsiniz: bu neden oluyor? Yaygın bir yanılgı vardır: "Çok iş parçacıklı programlama çok zordur." Ama değil. Çok iş parçacıklı bir program güvenilir değilse, genellikle düşük kaliteli tek iş parçacıklı programlarla aynı nedenlerle çöker. Sadece programcı temel, uzun süredir bilinen ve kanıtlanmış geliştirme yöntemlerini takip etmiyor. Çok iş parçacıklı programlar daha karmaşık görünüyor, çünkü ne kadar çok paralel iş parçacığı yanlış giderse, o kadar fazla karışıklık yaratırlar - ve tek bir iş parçacığından çok daha hızlıdır.

Tek iş parçacıklı kod yazmayı profesyonel olarak geliştiren, ilk önce çoklu iş parçacığıyla karşılaşan ve onunla baş edemeyen geliştiriciler nedeniyle "çok iş parçacıklı programlamanın zorluğu" hakkındaki yanılgı yaygınlaştı. Ancak önyargılarını ve alışılmış çalışma yöntemlerini yeniden gözden geçirmek yerine, hiçbir şekilde çalışmak istemediklerini inatla düzeltirler. Güvenilmez yazılımlar ve kaçırılan teslim tarihleri ​​için kendilerini haklı çıkaran bu insanlar aynı şeyi tekrarlıyor: "çok kanallı programlama çok zor."

Lütfen yukarıda çoklu kullanım kullanan tipik programlardan bahsettiğimi unutmayın. Gerçekten de, karmaşık çok iş parçacıklı senaryoların yanı sıra karmaşık tek iş parçacıklı senaryolar da vardır. Ama onlar nadirdir. Kural olarak, pratikte, bir programcıdan doğaüstü hiçbir şey gerekli değildir. Verileri taşır, dönüştürür, zaman zaman bazı hesaplamalar yapar ve son olarak bilgiyi bir veritabanında saklar veya ekranda gösteririz.

Ortalama tek iş parçacıklı bir programı geliştirmek ve onu çok iş parçacıklı bir programa dönüştürmek zor değildir. En azından olmamalı. Zorluklar iki nedenden dolayı ortaya çıkar:

  • programcılar basit, iyi bilinen kanıtlanmış geliştirme yöntemlerini nasıl uygulayacaklarını bilmiyorlar;
  • Çok iş parçacıklı programlama kitaplarında sunulan bilgilerin çoğu teknik olarak doğrudur, ancak uygulamalı sorunları çözmek için tamamen uygulanamaz.

En önemli programlama kavramları evrenseldir. Tek iş parçacıklı ve çok iş parçacıklı programlara eşit olarak uygulanırlar. Bir iş parçacığı girdabında boğulan programcılar, tek iş parçacıklı kod öğrenirken önemli dersleri öğrenmediler. Bunu söyleyebilirim çünkü bu tür geliştiriciler çok iş parçacıklı ve tek iş parçacıklı programlarda aynı temel hataları yapıyorlar.

Altmış yıllık programlama tarihinde öğrenilmesi gereken belki de en önemli ders şu şekildedir: küresel değişken durum- fenalık. Gerçek kötülük. Küresel değişebilir duruma bağlı olan programlar, durumlarını değiştirmenin çok fazla yolu olduğundan, akıl yürütmeleri nispeten zordur ve genellikle güvenilmezdir. Bu genel ilkeyi destekleyen bir ton araştırma var ve asıl amacı şu ya da bu şekilde veri gizlemeyi uygulamak olan sayısız tasarım modeli var. Programlarınızı daha öngörülebilir hale getirmek için değiştirilebilir durumu mümkün olduğunca ortadan kaldırmaya çalışın.

Tek iş parçacıklı sıralı bir programda, verilerin bozulma olasılığı, bu verileri değiştirebilecek bileşenlerin sayısı ile doğru orantılıdır.

Kural olarak, küresel durumdan tamamen kurtulmak mümkün değildir, ancak geliştirici, cephanelikte hangi program bileşenlerinin durumu değiştirebileceğini kesinlikle kontrol etmenizi sağlayan çok etkili araçlara sahiptir. Ayrıca, ilkel veri yapıları etrafında kısıtlayıcı API katmanlarının nasıl oluşturulacağını öğrendik. Bu nedenle, bu veri yapılarının nasıl değiştiği üzerinde iyi bir kontrole sahibiz.

Küresel değişken durumun sorunları, 80'lerin sonunda ve 90'ların başında, olay güdümlü programlamanın yükselişiyle birlikte yavaş yavaş belirgin hale geldi. Programlar artık "baştan" başlamadı ve "sonuna kadar" tek öngörülebilir yürütme yolunu takip etti. Modern programlar, çıktıktan sonra, içlerinde meydana gelen olayların - değişken zaman aralıklarıyla öngörülemeyen bir sırada - bir başlangıç ​​durumuna sahiptir. Kod tek iş parçacıklı kalır, ancak zaten zaman uyumsuz hale gelir. Olayların meydana gelme sırası çok önemli olduğundan, verilerin bozulma olasılığı tam olarak artar. Her zaman bunun gibi durumlar vardır: B olayı A olayından sonra meydana gelirse, her şey yolunda gider. Ancak A olayı B olayından sonra meydana gelirse ve C olayının aralarında sıkışmak için zamanı varsa, veriler tanınmayacak şekilde bozulabilir.

Paralel iş parçacıkları söz konusuysa, aynı anda birden çok yöntem global durumda çalışabileceğinden sorun daha da kötüleşir. Küresel devletin nasıl değiştiğini tam olarak yargılamak imkansız hale geliyor. Sadece olayların öngörülemeyen bir sırada meydana gelebilmesi değil, aynı zamanda birkaç yürütme iş parçacığının durumunun da güncellenebilmesi zaten mümkün. eşzamanlı. Asenkron programlama ile en azından belirli bir olayın başka bir olayın işlenmesi bitene kadar gerçekleşmeyeceğini garanti edebilirsiniz. Yani belirli bir olayın işlenmesi sonunda küresel durumun ne olacağını kesin olarak söylemek mümkündür. Çok iş parçacıklı kodda, hangi olayların paralel olarak gerçekleşeceğini söylemek genellikle imkansızdır, bu nedenle herhangi bir zamanda küresel durumu kesin olarak tanımlamak imkansızdır.

Kapsamlı küresel değişken durumuna sahip çok iş parçacıklı bir program, bildiğim Heisenberg Belirsizlik İlkesinin en açıklayıcı örneklerinden biridir. Davranışını değiştirmeden bir programın durumunu kontrol etmek imkansızdır.

Küresel değişebilir durum hakkında başka bir filippiye başladığımda (öz önceki birkaç paragrafta özetlenmiştir), programcılar gözlerini deviriyor ve tüm bunları uzun zamandır bildiklerine dair beni temin ediyor. Ama bunu biliyorsanız - neden kodunuzdan söylemiyorsunuz? Programlar global değişken durumuyla doldurulur ve programcılar kodun neden çalışmadığını merak eder.

Şaşırtıcı olmayan bir şekilde, çok iş parçacıklı programlamadaki en önemli çalışma tasarım aşamasında gerçekleşir. Programın ne yapması gerektiğini açıkça tanımlaması, tüm işlevleri yerine getirmek için bağımsız modüller geliştirmesi, hangi modülün hangi verileri gerektirdiğini ayrıntılı olarak tanımlaması ve modüller arasında bilgi alışverişinin yollarını belirlemesi gerekir ( Evet, tüm proje katılımcıları için birbirinden güzel tişörtler hazırlamayı unutmayın. İlk şey.- yaklaşık ed. orijinalinde). Bu süreç, tek iş parçacıklı bir program tasarlamaktan temelde farklı değildir. Tek iş parçacıklı kodda olduğu gibi başarının anahtarı, modüller arasındaki etkileşimleri sınırlamaktır. Paylaşılan değişken durumdan kurtulabilirsek, veri paylaşımı sorunları ortaya çıkmaz.

Bazen, küresel durumdan kaçınmak için bir programın daha ince tasarımı için zamanın olmadığı iddia edilebilir. Bunun zaman harcanabileceğine ve harcanması gerektiğine inanıyorum. Hiçbir şey çok iş parçacıklı programlara küresel değişken durumla uğraşmaya çalışmaktan daha fazla zarar vermez. Yönetmek zorunda olduğunuz daha fazla ayrıntı, programınızın bir kuyruk dönüşüne girmesi ve çökmesi olasılığı daha yüksektir.

Gerçekçi uygulama programlarında, değişebilen bazı ortak durumlar olmalıdır. Ve çoğu programcının sorun yaşadığı yer burasıdır. Programcı burada paylaşılan durumun gerekli olduğunu görür, çok iş parçacıklı cephaneliğe döner ve oradan en basit aracı alır: evrensel bir kilit (kritik bölüm, muteks veya ne diyorlarsa). Karşılıklı dışlamanın tüm veri paylaşımı sorunlarını çözeceğini düşünüyorlar.

Böyle tek bir kilitle ortaya çıkabilecek sorunların sayısı şaşırtıcıdır. Yarış koşulları, aşırı blokajlı geçit sorunları ve adalet sorunları sadece birkaç örnektir. Birden çok kilidiniz varsa, özellikle de iç içe geçmişlerse, kilitlenmelere, dinamik kilitlenmelere, kilit kuyruklarına ve eşzamanlılıkla ilgili diğer tehditlere de dikkat etmeniz gerekir. Ek olarak, tekli engellemenin karakteristik sorunları vardır.
Kodu yazdığımda veya gözden geçirdiğimde, neredeyse kırılmaz katı bir kurala uyuyorum: bir engelleme yaptıysanız, görünüşe göre, bir yerde bir hata yaptınız.

Bu ifade iki şekilde yorumlanabilir:

  1. Bir kilide ihtiyacınız varsa, muhtemelen eşzamanlı güncellemelerden korunması gereken küresel bir değişken durumunuz vardır. Global değişken duruma sahip olmak, uygulamanın tasarım aşamasında yapılmış bir kusurdur. Gözden geçirin ve yeniden tasarlayın.
  2. Engellemenin doğru şekilde kullanılması kolay değildir ve engellemeyle ilgili hataların yerelleştirilmesi inanılmaz derecede zor olabilir. Kilidi yanlış kullanmanız çok olasıdır. Bir kilit görürsem ve program olağandışı davranıyorsa, yaptığım ilk şey kilide bağlı olan kodu kontrol etmektir. Ve genellikle onunla ilgili sorunlar buluyorum.

Bu yorumların ikisi de doğrudur.

Çok iş parçacıklı kod yazmak kolaydır. Ancak senkronizasyon ilkellerini doğru kullanmak çok, çok zor. Tek bir kilidi bile doğru şekilde kullanmak için kalifiye olmayabilirsiniz. Sonuçta, kilitler ve diğer senkronizasyon ilkelleri, tüm sistem düzeyinde dikilmiş yapılardır. Paralel programlamayı sizden çok daha iyi anlayan kişiler, eşzamanlı veri yapıları ve üst düzey senkronizasyon yapıları oluşturmak için bu temel öğeleri kullanır. Ve biz sıradan programcılar, bu tür yapıları alıp kodumuzda kullanırız. Bir uygulama programcısı, aygıt sürücülerine doğrudan çağrı yaptığı gibi, düşük düzeyli eşitleme temel öğelerini de kullanmalıdır. Yani neredeyse hiç.

Veri paylaşımı sorunlarını çözmek için kilitleri kullanmaya çalışmak, sıvı oksijenle yangını söndürmeye benzer. Bir yangın gibi, bu sorunları önlemek, düzeltmekten daha kolaydır. Paylaşılan durumdan kurtulursanız, senkronizasyon ilkellerini de kötüye kullanmanız gerekmez.

Çoklu kullanım hakkında bildiklerinizin çoğu alakasız

Yeni başlayanlar için çoklu iş parçacığı eğitimlerinde, iş parçacıklarının ne olduğunu öğreneceksiniz. Daha sonra yazar, bu iş parçacıklarının paralel olarak çalışabileceği çeşitli yolları düşünmeye başlayacaktır - örneğin, paylaşılan verilere kilitler ve semaforlar kullanarak erişim kontrolü hakkında konuşacak, olaylarla çalışırken neler olabileceği üzerinde duracaktır. Koşul değişkenlerini, bellek engellerini, kritik bölümleri, muteksleri, geçici alanları ve atomik işlemleri ayrıntılı olarak düşünün. Her türlü sistem işlemini gerçekleştirmek için bu düşük seviyeli yapıların nasıl kullanılacağına dair örneklere bakacağız. Bu materyali yarıya kadar okuduktan sonra, programcı tüm bu ilkeller ve uygulamaları hakkında zaten yeterince bilgi sahibi olduğuna karar verir. Sonuçta sistem seviyesinde bu şeyin nasıl çalıştığını biliyorsam uygulama seviyesinde de aynı şekilde uygulayabilirim. Evet?

Bir gence içten yanmalı bir motorun nasıl inşa edileceğini söylediğinizi hayal edin. Sonra, herhangi bir sürüş talimatı olmadan, onu bir arabanın direksiyonuna geçirip "Sür" diyorsunuz! Genç arabanın nasıl çalıştığını anlıyor ama onu A noktasından B noktasına nasıl götüreceği hakkında hiçbir fikri yok.

İş parçacıklarının sistem düzeyinde nasıl çalıştığını anlamak, genellikle bunları uygulama düzeyinde kullanmaya hiçbir şekilde yardımcı olmaz. Programcıların tüm bu düşük seviyeli ayrıntıları öğrenmesine gerek olmadığını söylemiyorum. Bir iş uygulaması tasarlarken veya geliştirirken bu bilgiyi hemen uygulayabilmeyi beklemeyin.

Giriş niteliğindeki iş parçacığı literatürü (ve ilgili akademik dersler) bu tür düşük seviyeli yapıları öğretmemelidir. En yaygın sorun sınıflarını çözmeye odaklanmalı ve geliştiricilere bu tür sorunların üst düzey özellikler kullanılarak nasıl çözüldüğünü göstermeliyiz. Prensip olarak, çoğu iş uygulaması son derece basit programlardır. Bir veya daha fazla giriş cihazından veri okurlar, bu veriler üzerinde bazı karmaşık işlemler gerçekleştirirler (örneğin, süreçte biraz daha fazla veri talep ederler) ve ardından sonuçları görüntülerler.

Genellikle bu tür programlar, yalnızca üç akışın kullanılmasını gerektiren sağlayıcı-tüketici modeline mükemmel şekilde uyar:

  • giriş akışı verileri okur ve giriş kuyruğuna yerleştirir;
  • işçi iş parçacığı giriş kuyruğundaki girişleri okur, işler ve sonuçları çıkış sırasına koyar;
  • Çıktı iş parçacığı, çıktı kuyruğundaki girdileri okur ve bunları kaydeder.

Bu üç iş parçacığı bağımsız olarak çalışır, aralarındaki iletişim kuyruk düzeyinde gerçekleşir.

Bu kuyruklar teknik olarak paylaşılan durum bölgeleri olarak düşünülebilse de, pratikte sadece kendi iç senkronizasyonlarına sahip olan iletişim kanallarıdır. Kuyruklar, birçok üretici ve tüketici ile aynı anda çalışmayı destekler, bunlara paralel olarak öğe ekleyebilir ve kaldırabilirsiniz.

Giriş, işleme ve çıkış adımları birbirinden izole edildiğinden, programın geri kalanını etkilemeden uygulamalarını değiştirmek kolaydır. Kuyruktaki veri türü değişmediği sürece, tek tek program bileşenlerini istediğiniz gibi yeniden düzenleyebilirsiniz. Ayrıca isteğe bağlı sayıda üretici ve tüketici kuyruğa katıldığı için diğer üreticileri/tüketicileri eklemek zor olmayacaktır. Aynı kuyruğa bilgi yazan düzinelerce girdi iş parçacığına veya girdi kuyruğundan bilgi alan ve verileri sindiren düzinelerce işçi iş parçacığına sahip olabiliriz. Tek bir bilgisayarda böyle bir model iyi ölçeklenir.

Fakat en önemlisi modern programlama dilleri ve kütüphanelerinin üretici-tüketici modelinde uygulama oluşturmayı oldukça kolaylaştırmasıdır. .NET'te Parallel Collections ve TPL Dataflow kitaplığını bulacaksınız. Java'nın bir Executor hizmetinin yanı sıra BlockingQueue ve java.util.concurrent ad alanından diğer sınıfları vardır. C++, iş parçacıklarıyla çalışmak için Boost kitaplığına ve Intel'den Thread Building Blocks kitaplığına sahiptir. Microsoft'tan Visual Studio 2013, zaman uyumsuz aracıları tanıttı. Benzer kütüphaneler Python, JavaScript, Ruby, PHP ve bildiğim kadarıyla diğer birçok dilde de mevcuttur. Kilitlere, semaforlara, koşul değişkenlerine veya diğer senkronizasyon ilkelerine hiç başvurmadan bu paketlerden herhangi biriyle bir üretici-tüketici uygulaması oluşturabilirsiniz.

Bu kitaplıklar, çok çeşitli eşitleme temel öğelerini serbestçe kullanır. Bu iyi. Listelenen kitaplıkların tümü, çoklu iş parçacığını ortalama bir programcıdan kıyaslanamayacak kadar iyi anlayan kişiler tarafından yazılmıştır. Böyle bir kitaplıkla çalışmak, çalışma zamanı dil kitaplığı kullanmakla hemen hemen aynıdır. Bu, Assembly dilinden ziyade yüksek seviyeli bir dilde programlama ile karşılaştırılabilir.

Sağlayıcı-tüketici modeli birçok örnekten sadece biridir. Yukarıdaki kitaplıklar, düşük seviyeli ayrıntılara girmeden birçok yaygın iş parçacıklı tasarım modelini uygulamak için kullanılabilecek sınıfları içerir. İş parçacıklarının tam olarak nasıl koordine edildiği ve senkronize edildiği konusunda çok az veya hiç endişe duymadan büyük ölçekli çok iş parçacıklı uygulamalar oluşturabilirsiniz.

Kitaplıklarla Çalışma

Bu nedenle, çok iş parçacıklı programların oluşturulması, tek iş parçacıklı eşzamanlı programların yazılmasından temelde farklı değildir. Kapsülleme ve veri gizlemenin önemli ilkeleri evrenseldir ve önemleri yalnızca birçok eşzamanlı iş parçacığı söz konusu olduğunda artar. Bu önemli hususları ihmal ederseniz, akışların düşük düzeyde ele alınmasıyla ilgili en kapsamlı bilgi bile sizi kurtarmaz.

Modern geliştiriciler, uygulama programlama düzeyinde birçok sorunu çözmek zorundadır, sistem düzeyinde neler olup bittiğini düşünmek için zaman yoktur. Uygulamalar ne kadar karmaşık hale gelirse, API katmanları arasında o kadar karmaşık ayrıntıların gizlenmesi gerekir. Bunu on yıldan fazla bir süredir yapıyoruz. Sistemin karmaşıklığını programcıdan gizleme kalitesinin, programcının modern uygulamalar yazmayı başarmasının temel nedeni olduğu söylenebilir. Bu konuda - bir kullanıcı arayüzü mesaj döngüsü uygulayarak, düşük seviyeli iletişim protokolleri oluşturarak, vb. sistemin karmaşıklığını gizlemiyor muyuz?

Benzer bir durum multithreading ile ortaya çıkar. Ortalama bir iş uygulaması programcısının karşılaşabileceği çok iş parçacıklı senaryoların çoğu zaten iyi biliniyor ve kitaplıklarda iyi uygulanıyor. Kütüphane işlevleri, eşzamanlılığın şaşırtıcı karmaşıklığını gizleme konusunda harika bir iş çıkarır. Bu kitaplıkların, UI öğesi kitaplıklarını, iletişim protokollerini ve sadece çalışan diğer birçok aracı kullandığınız şekilde kullanmayı öğrenmesi gerekir. Düşük seviyeli çoklu iş parçacığını uzmanlara bırakın - uygulama programları oluşturmak için kullanılan kitaplıkların yazarları.

Andrey Kolesov

Microsoft .NET Framework için çok iş parçacıklı uygulamalar oluşturma ilkelerini düşünmeye başlayarak, hemen bir rezervasyon yapalım: Tüm örnekler Visual Basic .NET'te verilmiş olsa da, bu tür programları oluşturma metodolojisi genellikle tüm programlama dilleri için aynıdır. C# dahil .NET'i destekler. VB, çok iş parçacıklı uygulamalar oluşturma teknolojisini göstermek için seçildi, çünkü bu aracın önceki sürümleri böyle bir fırsat sağlamadı.

Dikkat: Visual Basic .NET BUNU da yapabilir!

Bildiğiniz gibi, Visual Basic (sürüm 6.0'a kadar ve dahil), çok iş parçacıklı program bileşenleri (EXE, ActiveX DLL ve OCX) oluşturmanıza asla izin vermedi. Burada COM mimarisinin üç farklı thread modeli içerdiğini unutmamalıyız: single-threaded (Single Thread), eklemli (Single Threaded Apartment, STA) ve free (Multi-Threaded Apartment). VB 6.0, ilk iki tür programı oluşturmanıza olanak tanır. STA varyantı sözde çoklu okuma sağlar - birkaç iş parçacığı gerçekten paralel çalışır, ancak her birinin program kodu dışarıdan erişime karşı korunur (özellikle iş parçacıkları paylaşılan kaynakları kullanamaz).

Visual Basic .NET artık gerçek (yerel) biçiminde ücretsiz çoklu kullanım uygulayabilir. Daha doğrusu, .NET'te bu mod, ortak sınıf kitaplıkları Sınıf Kitaplığı ve Ortak Dil Çalışma Zamanı düzeyinde desteklenir. Sonuç olarak, VB.NET, diğer .NET programlama dilleriyle birlikte bu özelliklere erişime sahiptir.

Bir zamanlar, bu dilde gelecekteki birçok yenilikten memnuniyetsizliğini ifade eden VB geliştirici topluluğu, aracın yeni sürümüyle çok iş parçacıklı programlar oluşturmanın mümkün olacağı haberine büyük bir onayla tepki gösterdi (bkz. Studio .NET", "BYTE /Rusya" No. 1/2001). Ancak, birçok uzman bu yenilik hakkında daha kısıtlı değerlendirmeler dile getirdi. Örneğin, VB programcıları için çok sayıda kitabın yazarı ve tanınmış bir geliştirici olan Dan Appleman'ın (Dan Appleman) görüşü şudur: teknolojik faktörlerden ziyade insan nedeniyle ... VB.NET'te çoklu kullanımdan korkuyorum, çünkü VB programcıları genellikle çok iş parçacıklı uygulamaları tasarlama ve hata ayıklama konusunda deneyime sahip değildir" .

Gerçekten de, diğer düşük seviyeli programlama araçları gibi (örneğin, aynı Win API arayüzleri), ücretsiz çoklu okuma, bir yandan yüksek performanslı ölçeklenebilir çözümler oluşturmak için daha fazla fırsat sağlarken, diğer yandan daha yüksek gereksinimler getirir. geliştiricilerin nitelikleri. Ayrıca, buradaki sorun, çok iş parçacıklı bir uygulamada hata aramanın çok zor olması gerçeğiyle daha da kötüleşir, çünkü bunlar genellikle paralel hesaplama işlemlerinin belirli bir kesişiminin bir sonucu olarak rastgele ortaya çıkarlar (bu tür yeniden oluşturmak genellikle imkansızdır). yine bir durum). Bu nedenle, programların yeniden çalıştırılması şeklindeki geleneksel hata ayıklama yöntemleri bu durumda genellikle yardımcı olmaz. Ve çoklu iş parçacığını güvenli bir şekilde kullanmanın tek yolu, tüm klasik "doğru programlama" ilkelerini izleyerek uygulamayı iyi tasarlamaktır.

VB programcılarının sorunu, birçoğunun oldukça deneyimli profesyoneller olmalarına ve çoklu kullanımdaki tuzakların çok iyi farkında olmalarına rağmen, VB6 kullanmanın uyanıklıklarını köreltebilmesidir. Sonuçta, sınırlamalar için VB'yi suçlarken, bazen sınırlamaların çoğunun, geliştirici hatalarını önleyen veya ortadan kaldıran bu aracın geliştirilmiş güvenlik özellikleri tarafından belirlendiğini unutuyoruz. Örneğin, VB6, her bir iş parçacığı için tüm global değişkenlerin ayrı bir kopyasını otomatik olarak oluşturur ve böylece aralarında olası çakışmaları önler. VB.NET'te bu tür sorunlar tamamen programcının omuzlarına kaydırılır. Tek iş parçacıklı yerine çok iş parçacıklı bir modelin kullanılmasının her zaman program performansında artışa yol açmadığı, performansın düşebileceği (çok işlemcili sistemlerde bile!)

Bununla birlikte, yukarıdakilerin tümü, çoklu iş parçacığı ile uğraşmamak için tavsiye olarak alınmamalıdır. Sadece bu tür modların ne zaman kullanılması gerektiği konusunda iyi bir fikre sahip olmanız ve daha güçlü bir geliştirme aracının programcının becerilerine her zaman daha yüksek talepler getirdiğini anlamanız gerekir.

VB6'da paralel işleme

Elbette, VB6 yardımıyla sözde paralel veri işlemeyi organize etmek mümkündü, ancak bu olanaklar çok sınırlıydı. Örneğin, birkaç yıl önce, bir programın yürütülmesini belirli bir süre için duraklatan bir prosedür yazmam gerekiyordu (ilgili SLEEP ifadesi Microsoft Basic/DOS'ta zaten mevcuttu). Aşağıdaki basit alt program şeklinde kendiniz uygulamak zor değildir:

Örneğin, bir formda bir düğme tıklamasını işlemek için aşağıdaki kodu kullanarak, çalıştığını kolayca doğrulayabilirsiniz:

VB6'da bu sorunu çözmek için, SleepVB prosedürünün Do...Loop'unda, denetimi işletim sistemine aktaran ve bu VB uygulamasında açık formların sayısını döndüren DoEvents işlevine yapılan çağrının yorumunu kaldırmanız gerekir. Ancak, "Bir merhaba daha!" mesajını içeren bir pencerenin görüntülenmesinin, SleepVB prosedürü de dahil olmak üzere tüm uygulamanın yürütülmesini engellediğini unutmayın.

Global değişkenleri bayrak olarak kullanarak, çalışan bir SleepVB rutininin sonlandırılmasını da sağlayabilirsiniz. Bu, işlemcinin kaynaklarını tamamen işgal eden bir hesaplama sürecinin en basit örneğidir. Ancak bazı yararlı hesaplamalar yapıyorsanız (ve boş bir döngüde dönmüyorsanız), DoEvent işlevini çağırmanın oldukça uzun sürdüğünü, bu nedenle bunun oldukça geniş zaman aralıklarında yapılması gerektiğini unutmayın.

VB6'nın paralel bilgi işlem desteğinin sınırlamalarını görmek için, DoEvents işlevine yapılan çağrıyı bir etiket çıktısıyla değiştirin:

Label1.Caption = Zamanlayıcı

Bu durumda, yalnızca Command2 düğmesi çalışmayacak, aynı zamanda 5 s içinde bile etiketin içeriği değişmeyecektir.

Başka bir deney için, Komut2 koduna bir bekleme çağrısı ekleyin (SleepVB prosedürü yeniden girişli olduğu için bunu yapabilirsiniz):

Private Sub Command2_Click() SleepVB(5)'i Çağır MsgBox "Tekrar merhaba!" son alt

Ardından, uygulamayı çalıştırın ve Komut1'i ve 2-3 saniye sonra - Komut2'yi tıklayın. İlgili işlem daha sonra başlatılmış olmasına rağmen, önce "Bir merhaba daha"! mesajı görünecektir. Bunun nedeni, DoEvents işlevinin diğer değerlendirme dizilerini değil, yalnızca görsel öğe olaylarını kontrol etmesidir. Ayrıca, VB uygulaması aslında tek bir iş parçacığı üzerinde çalışıyor, bu nedenle kontrol, başlatılan son olay prosedürüne geri döndü.

.NET'te Konu Yönetimi

Çok iş parçacıklı .NET uygulamaları oluşturmak, System.Threading ad alanı tarafından açıklanan bir grup .NET Framework temel sınıfının kullanımına dayanır. Bu durumda, anahtar rol, neredeyse tüm iş parçacığı yönetimi işlemlerinin gerçekleştirildiği Thread sınıfına aittir. Bu noktadan itibaren, iş parçacıklarıyla çalışma hakkında söylenen her şey, C# dahil .NET'teki tüm programlama araçları için geçerlidir.

Paralel iş parçacığı oluşturma ile ilk tanışma için, ButtonStart ve ButtonAbort düğmelerini yerleştirdiğimiz bir form ile bir Windows uygulaması oluşturalım ve aşağıdaki kodu yazalım:

Hemen üç noktaya dikkatinizi çekmek istiyorum. İlk olarak, Imports anahtar sözcükleri, buradaki ad alanı tarafından açıklanan sınıfların kısaltılmış adlarına atıfta bulunmak için kullanılır. Program metnine uygulanabilecek uzun ad alanı adının (VB = Microsoft.VisualBasic) kısaltılmış eşdeğerini açıklamak için özel olarak Imports'un başka bir kullanımını verdim. Bu durumda, Timer nesnesinin hangi ad alanına ait olduğunu hemen görebilirsiniz.

İkinci olarak, yazdığım kodu, form tasarımcısı tarafından otomatik olarak oluşturulan koddan görsel olarak ayırmak için #Region mantıksal parantezlerini kullandım (ikincisi burada gösterilmemiştir).

Üçüncü olarak, olay prosedürlerinin girdi parametrelerinin açıklamaları, bu durumda önemli olmayan şeyler tarafından dikkatin dağılmaması için özel olarak kaldırılır (bu bazen gelecekte yapılacaktır).

Uygulamayı çalıştırın ve ButtonStart düğmesine tıklayın. Belirli bir zaman aralığındaki bir döngüde bekleme süreci başladı ve bu durumda (VB6 örneğinden farklı olarak) - bağımsız bir iş parçacığında. Bunu doğrulamak kolaydır - formun tüm görsel öğelerine erişilebilir. Örneğin, ButtonAbort düğmesine basarak, Abort yöntemini kullanarak işlemi iptal edebilirsiniz (ancak formu, sistemin Kapat düğmesiyle kapatmak, prosedürü iptal etmez!). Sürecin dinamiklerini görselleştirmek için forma bir etiket yerleştirebilir ve mevcut zamanın çıktısını SleepVBNET prosedür bekleme döngüsüne ekleyebilirsiniz:

Label1.Text = _ "Geçerli saat = " & VB.TimeOfDay

İplik başladıktan sonra hesaplama başlatma mesaj kutusunu görüntülemek için ButtonStart koduna bir mesaj kutusu ekleseniz bile SleepVBNET prosedürünün yürütülmesi (bu durumda zaten yeni nesnenin bir yöntemidir) devam edecektir (Şekil 1).

Daha karmaşık bir seçenek - sınıf biçiminde bir akış

İş parçacıkları üzerinde daha fazla deneme yapmak için, bir Main yordamı (uygulama başladığında yürütülmeye başlar) ve bir WorkerThreadClass sınıf modülüne sahip normal bir kod modülünden oluşan Konsol türünde yeni bir VB uygulaması oluşturalım:

Oluşturulan uygulamayı çalıştıralım. Çalışan bir karakter satırının görüneceği ve çalışan hesaplama sürecinin (WorkerThread) modelini gösteren bir konsol penceresi görünecektir. Ardından, arama işlemi (Main) tarafından verilen mesaj kutusu görünecek ve son olarak Şekil 2'de gösterilen resmi göreceğiz. 2 (Simüle edilen işlemin hızından memnun değilseniz, WorkerThread prosedüründe "a" değişkeni ile bazı aritmetik işlemleri kaldırın veya ekleyin).

WorkerThread işlemi başladıktan sonra "İlk İş parçacığı Başlatıldı" mesaj kutusunun gözle görülür bir gecikmeyle görüntülendiğini unutmayın (önceki paragrafta açıklanan form durumunda, böyle bir mesaj ButtonStart düğmesine bastıktan hemen sonra görünürdü). Bunun nedeni büyük olasılıkla olay prosedürlerinin formla çalışırken çalışan süreçten önce gelmesidir. Konsol uygulaması durumunda, tüm prosedürler aynı önceliğe sahiptir. Öncelikleri daha sonra tartışacağız, ancak şimdilik çağıran ileti dizisini (Ana) en yüksek önceliğe ayarlayın:

Thread.CurrentThread.Priority = _ ThreadPriority.En Yüksek Thread1.Start()

Şimdi pencere neredeyse anında belirir. Gördüğünüz gibi, Thread nesnesini başlatmanın iki yolu vardır. İlk olarak, ilkini uyguladık - yeni bir nesne (thread) Thread1 yarattık ve onunla çalıştık. İkinci seçenek, CurrentThread statik yöntemini kullanarak o anda yürütülen iş parçacığı için Thread nesnesini elde etmektir. Ana prosedür bu şekilde kendisi için daha yüksek bir öncelik belirler, ancak bunu başka herhangi bir iş parçacığı için yapabilirdi, örneğin:

Thread1.Priority = ThreadPriority.En Düşük Thread1.Start()

Çalışan bir işlemi yönetme olanaklarını göstermek için Ana prosedürün sonuna aşağıdaki kod satırlarını ekleyin:

Şimdi aynı anda bazı fare işlemlerini yaparken uygulamayı başlatın (Umarım WorkerThread'de doğru gecikme seviyesini seçmişsinizdir, böylece süreç çok hızlı değil, çok yavaş da olmaz).

İlk olarak, konsol penceresinde "İşlem 1" başlayacak ve "İlk iş parçacığı başlatıldı" mesajı görünecektir. "İşlem 1" çalışıyor ve mesaj kutusunda hızlıca Tamam'ı tıklıyorsunuz.

Ayrıca - "İşlem 1" devam eder, ancak iki saniye sonra "İplik askıya alındı" mesajı görünür. "Süreç 1" dondu. Mesaj kutusundaki "Tamam" düğmesini tıklayın: "İşlem 1" yürütmeye devam etti ve başarıyla tamamlandı.

Bu snippet'te, mevcut işlemi askıya almak için Sleep yöntemini kullandık. Sleep'in statik bir yöntem olduğunu ve Thread nesnesinin herhangi bir örneğine değil, yalnızca geçerli işleme uygulanabileceğini unutmayın. Dilin sözdizimi, Thread1.Sleep veya Thread.Sleep yazmanıza izin verir, ancak bu durumda CurrentThread nesnesi hala kullanılır.

Sleep yöntemi ayrıca 0 argümanını da kullanabilir. Bu durumda, geçerli iş parçacığı, ayrılan zaman diliminin kullanılmayan kalanını serbest bırakır.

Sleep için başka bir ilginç kullanım durumu, Timeout.Infinite değeridir. Bu durumda, iş parçacığı, Thread.Interrupt yöntemi kullanılarak bu durum başka bir iş parçacığı tarafından kesilene kadar süresiz olarak askıya alınacaktır.

Başka bir iş parçacığını durdurmadan bir dış iş parçacığını askıya almak için, Thread.Suspend yöntemine bir çağrı kullanmanız gerekir. Daha sonra yukarıdaki kodda yaptığımız Thread.Resume metodunu kullanarak yürütmeye devam etmek mümkün olacaktır.

İş parçacığı senkronizasyonu hakkında biraz

İş parçacığı senkronizasyonu, çok iş parçacıklı uygulamalar yazarken en büyük zorluklardan biridir ve System.Threading alanı bunu ele almak için çok sayıda araç içerir. Ancak şimdi yalnızca, iş parçacığının yürütülmesinin sonunu izlemenizi sağlayan Thread.Join yöntemiyle tanışacağız. Nasıl çalıştığını görmek için Ana prosedürün son satırlarını bu kodla değiştirin:

Süreç Önceliği Yönetimi

İşlemci zaman dilimlerinin iş parçacıkları arasında dağılımı, Thread.Priority özelliği biçiminde ayarlanan öncelikler kullanılarak gerçekleştirilir. Çalışma zamanında oluşturulan iş parçacıkları beş değere ayarlanabilir: En Yüksek, Normalin Üstünde, Normal (varsayılan), Normalin Altında ve En Düşük. Önceliklerin threadlerin yürütme hızını nasıl etkilediğini görmek için Main yordamı için aşağıdaki kodu yazalım:

Sub Main() " ilk işlemin açıklaması Dim Thread1 As Thread Dim oWorker1 As New WorkerThreadClass() Thread1 = New Thread(AddressOf _ oWorker1.WorkerThread) " Thread1.Priority = _ " ThreadPriority.BelowNormal " ilk veriyi iletir: oWorker1.Start = 1 oWorker1.Finish = 10 oWorker1.ThreadName = "Geri sayım 1" oWorker1.SymThread = "." " ikinci işlemin açıklaması Dim Thread2 As Thread Dim oWorker2 As New WorkerThreadClass() Thread2 = New Thread(AddressOf _ oWorker2.WorkerThread) " ilk verileri iletir: oWorker2.Start = 11 oWorker2.Finish = 20 oWorker2.ThreadName = "Countdown 2" oWorker2 .SymThread = "*" " " yarışmaya başla Thread.CurrentThread.Priority = _ ThreadPriority.Highest Thread1.Start() Thread2.Start() " İşlemlerin bitmesi bekleniyor Thread1.Join() Thread2.Join() MsgBox ("Her iki işlem de tamamlandı ") End Sub

Burada birden çok iş parçacığı oluşturmak için bir sınıfın kullanıldığını unutmayın. Uygulamayı çalıştıralım ve iki iş parçacığının yürütülmesinin dinamiklerine bakalım (Şekil 3). Burada genel olarak aynı hızda yürütüldüklerini görebilirsiniz, ilki daha erken lansman nedeniyle biraz ileride.

Şimdi, ilk iş parçacığına başlamadan önce önceliğini bir seviye aşağıya ayarlayın:

Thread1.Priority = _ ThreadPriority.BelowNormal

Resim çarpıcı bir şekilde değişti: ikinci akış, ilkinden neredeyse tüm zamanını aldı (Şekil 4).

Ayrıca Join yönteminin kullanımına da dikkat edin. Yardımı ile, ana programın birkaç paralel hesaplama işleminin tamamlanmasını beklediği oldukça yaygın bir iş parçacığı senkronizasyonu sürümü gerçekleştiriyoruz.

Çözüm

Biz sadece çok iş parçacıklı .NET uygulamaları geliştirmenin temellerine değindik. En karmaşık ve pratikte güncel konulardan biri, iş parçacıklarının senkronizasyonudur. Bu makalede açıklanan Thread nesnesini kullanmaya ek olarak (burada ele almadığımız birçok yöntem ve özelliği vardır), Monitor ve Mutex sınıfları ile lock (C#) ve SyncLock (VB.NET) operatörleri bir iş parçacığı yönetiminde çok önemli bir rol.

Bu teknolojinin daha ayrıntılı bir açıklaması kitapların ayrı bölümlerinde verilmiştir ve bunlardan birkaçını (ki buna tamamen katılıyorum) ".NET'te Çoklu Okuma" konusunun çok kısa bir özeti olarak alıntılamak istiyorum.

"Eğer yeniyseniz, iş parçacığı oluşturma ve gönderme ek yükünün tek iş parçacıklı bir uygulamayı daha hızlı çalıştırabilmesi sizi şaşırtabilir... Bu nedenle her zaman hem tek iş parçacıklı hem de çok iş parçacıklı program prototiplerini test etmeye çalışın. "

"İş parçacığı tasarımı konusunda dikkatli olmalı ve paylaşılan nesnelere ve değişkenlere erişimi sıkı bir şekilde kontrol etmelisiniz."

"Çoklu okuma, varsayılan yaklaşım olarak kabul edilmemelidir".

"Deneyimli VB programcılarından oluşan bir izleyici kitlesine, VB'nin gelecekteki bir sürümünde ücretsiz çoklu kullanım almak isteyip istemediklerini sordum. Hemen hemen herkes elini kaldırdı. Sonra kimin neye bulaştığını bildiğini sordum. Bu sefer sadece birkaç kişi ellerini kaldırdı. ve yüzlerinde bilmiş gülümsemeler vardı.

"Çok iş parçacıklı uygulamalar tasarlamanın getirdiği zorluklardan korkmuyorsanız, doğru kullanıldığında çoklu iş parçacığı, bir uygulamanın performansını büyük ölçüde artırabilir" .

Kendi başıma, çok iş parçacıklı .NET uygulamaları yaratma teknolojisinin (diğer birçok .NET teknolojisi gibi) bir bütün olarak pratikte kullanılan dilden bağımsız olduğunu da eklemek isterim. Bu nedenle, geliştiricilere, şu veya bu teknolojiyi göstermek için hangi programlama dilinin seçildiğine bakılmaksızın, farklı kitaplar ve makaleler incelemelerini öneriyorum.

Edebiyat:

  1. Dan Appleman. VB.NET'e geçiş: stratejiler, kavramlar, kod / Per. İngilizceden. - St. Petersburg: "Peter", 2002, - 464 s.: hasta.
  2. Tom Archer. C#'ın Temelleri. En son teknolojiler / Per. İngilizceden. - M .: Yayın ve ticaret evi "Rus Baskısı", 2001. - 448 s.: hasta.

Çoklu görev ve çoklu iş parçacığı

Bu basit ifadeyle başlayalım: 32 bit Windows işletim sistemleri, çoklu görev (çoklu işlem) ve çok iş parçacıklı veri işleme modlarını destekler. Bunu ne kadar iyi yaptıklarını tartışabilirsiniz, ama bu başka bir konu.

Çoklu görev, bir bilgisayarın aynı anda birkaç görevi paralel olarak gerçekleştirebildiği bir çalışma modudur. Bir bilgisayarın bir işlemcisi varsa, işletim sisteminin bazı kurallara göre hızlı bir şekilde farklı görevler arasında geçiş yapabileceği sözde paralellikten bahsediyoruz. Görev, bazı mantıksal eylemleri gerçekleştiren ve işletim sisteminin kaynakları tahsis ettiği bir birim olan bir program veya programın (uygulamanın) bir parçasıdır. Biraz basitleştirilmiş bir biçimde, Windows'ta ayrı bir yürütülebilir modül (EXE, DLL) olarak uygulanan her yazılım bileşeninin bir görev olduğunu varsayabiliriz. Windows için, "görev" kavramı, özellikle, program kodunun kesinlikle kendisine tahsis edilen adres alanında yürütülmesi anlamına gelen "süreç" ile aynı anlama sahiptir.

İki ana çoklu görev türü vardır - ortak (kooperatif) ve önleyici (önleyici). Windows'un önceki sürümlerinde uygulanan ilk seçenek, yalnızca etkin görev işletim sistemine eriştiğinde (örneğin, G / Ç için) görevler arasında geçiş yapılmasını sağlar. Bu durumda, her bir iş parçacığı, kontrolü işletim sistemine geri vermekten sorumludur. Görev böyle bir işlemi yapmayı unutursa (örneğin, döngüye girdi), bu genellikle tüm bilgisayarın donmasına neden oldu.

Önleyici çoklu görev, işletim sisteminin her bir iş parçacığına gereken kuantum zamanını (zaman dilimi) vermekten sorumlu olduğu ve ardından (başka görevlerden gelen istekler varsa) bu iş parçacığını otomatik olarak kestiği ve bir sonraki adımın ne olacağına karar verdiği bir moddur. Daha önce, bu moda "zaman paylaşımı" deniyordu.

Akış nedir? Bir iş parçacığı, özerk bir hesaplama işlemidir, ancak işletim sistemi düzeyinde değil, bir görev içinde tahsis edilir. Bir iş parçacığı ve bir "işlem görevi" arasındaki temel fark, bir görevin tüm iş parçacıklarının tek bir adres alanında yürütülmesidir, yani paylaşılan bellek kaynaklarıyla çalışabilirler. Bu tam olarak onların avantajları (paralel veri işleme) ve dezavantajlarıdır (program güvenilirliğine yönelik tehdit). Burada, çoklu görev durumunda, işletim sisteminin öncelikle uygulamaları korumaktan ve çoklu iş parçacığı kullanırken geliştiricinin kendisinden sorumlu olduğu akılda tutulmalıdır.

Tek işlemcili sistemlerde çoklu görev modunun kullanılmasının, bir bütün olarak çoklu görev sisteminin genel performansını artırmayı mümkün kıldığını unutmayın (her zaman olmasa da, anahtarların sayısı arttıkça, işletim sistemi tarafından işgal edilen kaynakların payı artar). ). Ancak, belirli bir görevin yürütme süresi, işletim sisteminin ek çalışması nedeniyle her zaman, çok az da olsa artar.

İşlemci görevlerle yoğun bir şekilde yüklenmişse (örneğin, tamamen matematiksel problemler durumunda minimum G/Ç boşta iken), gerçek genel performans artışı yalnızca çok işlemcili sistemler kullanıldığında elde edilir. Bu tür sistemler, farklı paralelleştirme modellerine izin verir - görev düzeyinde (her görev yalnızca bir işlemciyi işgal ederken, iş parçacıkları yalnızca sözde paralel olarak yürütülür) veya iş parçacığı düzeyinde (bir görev, iş parçacıklarıyla birkaç işlemciyi işgal ettiğinde).

Burada, atası 60'ların sonlarında IBM System / 360 ailesi olan toplu kullanım için güçlü bilgi işlem sistemlerinin çalışmasında, en acil görevlerden birinin, çoklu görev yönetimi için en uygun varyantın seçimi olduğu hatırlanabilir. - çeşitli parametreleri dikkate alarak dinamik mod dahil. Temel olarak, çoklu görevi yönetmek işletim sisteminin bir işlevidir. Ancak, bir veya başka bir seçeneğin uygulanmasının etkinliği, bir bütün olarak bilgisayar mimarisinin ve özellikle işlemcinin özellikleriyle doğrudan ilgilidir. Örneğin, aynı yüksek performanslı IBM System / 360, iş görevleri alanındaki paylaşılan sistemlerde mükemmel çalıştı, ancak aynı zamanda "gerçek zamanlı" sınıf problemlerini çözmek için tamamen uygun değildi. DEC PDP 11/20 gibi çok daha ucuz ve daha basit mini bilgisayarlar o zamanlar bu alanda açıkça liderdi.

dosyanın sonu. Böylece farklı işlemler tarafından yapılan log girişleri hiçbir zaman karıştırılmaz. Daha modern Unix sistemleri, günlük kaydı için özel bir syslog(3C) hizmeti sağlar.

Avantajlar:

  1. Geliştirme kolaylığı. Aslında, tek iş parçacıklı bir uygulamanın birçok kopyasını çalıştırıyoruz ve bunlar birbirinden bağımsız olarak çalışıyor. Belirli bir çok iş parçacıklı API'leri kullanmamak mümkündür ve süreçler arası iletişim araçları.
  2. Yüksek güvenilirlik. Süreçlerden herhangi birinin çökmesi, diğer süreçleri hiçbir şekilde etkilemez.
  3. İyi tolerans. Uygulama herhangi bir üzerinde çalışacak çoklu görev işletim sistemi
  4. Yüksek güvenlik. Farklı uygulama süreçleri farklı kullanıcılar olarak çalışabilir. Bu şekilde, süreçlerin her biri yalnızca çalışması için gerekli olan haklara sahip olduğunda, en az ayrıcalık ilkesi uygulanabilir. Uzaktan kod yürütülmesine izin veren işlemlerden birinde bir hata bulunsa bile, saldırgan yalnızca bu işlemin yürütüldüğü erişim seviyesini elde edebilecektir.

Kusurlar:

  1. Tüm uygulama görevleri bu şekilde sağlanamaz. Örneğin, bu mimari, statik HTML sayfalarına hizmet eden bir sunucu için uygundur, ancak bir veritabanı sunucusu ve birçok uygulama sunucusu için tamamen uygun değildir.
  2. İşlemleri oluşturmak ve yok etmek pahalı bir işlemdir, bu nedenle bu mimari birçok görev için yetersizdir.

Unix sistemleri, bir süreç oluşturmayı ve süreç içinde yeni bir program çalıştırmayı mümkün olduğunca ucuz hale getirmek için bir dizi önlem alır. Ancak, mevcut bir süreç içinde bir iş parçacığı oluşturmanın her zaman yeni bir süreç oluşturmaktan daha ucuz olacağını anlamalısınız.

Örnekler: apache 1.x (HTTP sunucusu)

Soketler, Borular ve System V IPC Mesaj Kuyrukları Aracılığıyla Haberleşen Çok İşlemli Uygulamalar

Listelenen IPC araçları (İşlemler arası iletişim), harmonik işlemler arası iletişimin sözde araçlarına atıfta bulunur. Paylaşılan bellek kullanmadan süreçlerin ve iş parçacıklarının etkileşimini düzenlemenize izin verirler. Programlama teorisyenleri bu mimariyi severler çünkü rekabet hatalarının birçok çeşidini neredeyse ortadan kaldırır.

Avantajlar:

  1. Göreceli geliştirme kolaylığı.
  2. Yüksek güvenilirlik. İşlemlerden biri çökerse, boru veya soket kapatılır ve mesaj kuyrukları durumunda, mesajlar artık kuyruğa giremez veya kuyruktan alınamaz. Uygulamanın geri kalanı bu hatayı kolayca algılayabilir ve muhtemelen (ancak zorunlu olarak değil) yalnızca başarısız işlemi yeniden başlatarak bu hatayı düzeltebilir.
  3. Bu uygulamaların çoğu (özellikle soketlere dayalı olanlar), uygulamanın farklı bileşenlerinin farklı makinelerde çalıştığı dağıtılmış bir ortamda çalışacak şekilde kolayca uyarlanabilir.
  4. İyi tolerans. Uygulama, eski Unix sistemleri de dahil olmak üzere çoğu çok görevli işletim sisteminde çalışacaktır.
  5. Yüksek güvenlik. Farklı uygulama süreçleri farklı kullanıcılar olarak çalışabilir. Bu şekilde, süreçlerin her biri yalnızca çalışması için gerekli olan haklara sahip olduğunda, en az ayrıcalık ilkesi uygulanabilir.

Uzaktan kod yürütülmesine izin veren işlemlerden birinde bir hata bulunsa bile, saldırgan yalnızca bu işlemin yürütüldüğü erişim seviyesini elde edebilecektir.

Kusurlar:

  1. Uygulanan tüm problemler için böyle bir mimariyi geliştirmek ve uygulamak kolay değildir.
  2. Listelenen tüm IPC araçları türleri, seri veri aktarımını içerir. Paylaşılan verilere rastgele erişim gerekiyorsa, bu mimari elverişsizdir.
  3. Verileri bir boru, soket ve mesaj kuyruğundan geçirmek, sistem çağrılarının yürütülmesini ve verilerin iki kez kopyalanmasını gerektirir - önce kaynak işlemin adres alanından çekirdeğin adres alanına, ardından çekirdeğin adres alanından belleğe hedef süreç. Bunlar pahalı işlemlerdir. Büyük miktarda veri aktarırken bu ciddi bir sorun haline gelebilir.
  4. Çoğu sistemin toplam boru, soket ve IPC sayısıyla ilgili sınırları vardır. Örneğin, Solaris'te varsayılan olarak, işlem başına 1024'ten fazla açık boruya, yuvaya ve dosyaya izin verilir (bu, belirli sistem çağrısının sınırlamalarından kaynaklanır). Solaris mimari sınırı, işlem başına 65536 boru, soket ve dosyadır.

    TCP/IP yuvalarının toplam sayısındaki sınır, ağ arabirimi başına 65536'dan fazla değildir (TCP başlıklarının biçimi nedeniyle). System V IPC mesaj kuyrukları, çekirdeğin adres alanında bulunur, bu nedenle bir sistemdeki kuyruk sayısı ve bir kerede sıraya alınabilecek mesajların boyutu ve sayısı konusunda kesin sınırlar vardır.

  5. Bir süreç yaratmak ve yok etmek ve aynı zamanda süreçler arasında geçiş yapmak pahalı işlemlerdir. Her durumda değil, böyle bir mimari optimaldir.

Paylaşılan Bellek Üzerinden Haberleşen Çok İşlemli Uygulamalar

Paylaşılan bellek, System V IPC paylaşılan belleği ve dosyadan belleğe eşleme olabilir. Erişimi senkronize etmek için, dosyaları belleğe eşlerken bir dosyanın bölümlerini yakalarken System V IPC semaforlarını, mutekslerini ve POSIX semaforlarını kullanabilirsiniz.

Avantajlar:

  1. Paylaşılan verilere verimli rastgele erişim. Bu mimari, veritabanı sunucularının uygulanması için uygundur.
  2. Yüksek tolerans. System V IPC'yi destekleyen veya taklit eden herhangi bir işletim sistemine taşınabilir.
  3. Nispeten yüksek güvenlik. Farklı uygulama süreçleri farklı kullanıcılar olarak çalışabilir. Bu şekilde, süreçlerin her biri yalnızca çalışması için gerekli olan haklara sahip olduğunda, en az ayrıcalık ilkesi uygulanabilir. Ancak, erişim seviyelerinin ayrımı, daha önce tartışılan mimarilerdeki kadar katı değildir.

Kusurlar:

  1. Gelişimin göreceli karmaşıklığı. Erişim senkronizasyonundaki hataların - sözde çekişme hataları - test sırasında tespit edilmesi çok zordur.

    Bu, tek iş parçacıklı veya daha basit çoklu görev mimarilerine kıyasla genel geliştirme maliyetinde 3 ila 5 kat artışla sonuçlanabilir.

  2. Düşük güvenilirlik. Uygulamanın süreçlerinden herhangi birinin çökmesi, paylaşılan belleği tutarsız bir durumda bırakabilir (ve çoğu zaman da yapar).

    Bu genellikle uygulamanın geri kalan görevlerinin çökmesine neden olur. Lotus Domino gibi bazı uygulamalar, herhangi birinin çökmesi durumunda tüm sunucu işlemlerini kasıtlı olarak öldürür.

  3. Bir süreç yaratmak ve yok etmek ve bunlar arasında geçiş yapmak pahalı işlemlerdir.

    Bu nedenle, bu mimari tüm uygulamalar için uygun değildir.

  4. Belirli koşullar altında, paylaşılan belleğin kullanımı ayrıcalık yükselmesine neden olabilir. Uzaktan kod yürütülmesine yol açan işlemlerden birinde bir hata bulunursa, bir saldırganın diğer uygulama işlemlerinde uzaktan kod yürütmek için bunu kullanması çok olasıdır.

    Diğer bir deyişle, en kötü durumda, bir saldırgan, uygulamanın süreçlerinin en yüksek erişim düzeyine karşılık gelen bir erişim düzeyi elde edebilir.

  5. Paylaşılan belleği kullanan uygulamalar aynı fiziksel bilgisayarda veya en azından paylaşılan RAM'e sahip makinelerde çalışmalıdır. Aslında bu sınırlama, örneğin bellek eşlemeli paylaşılan dosyalar kullanılarak aşılabilir, ancak bu, önemli bir ek yük getirir.

Aslında, bu mimari, çok işlemli ve çok iş parçacıklı uygulamaların dezavantajlarını uygun şekilde birleştirir. Bununla birlikte, 80'lerde ve 90'ların başında, çok iş parçacıklı API'ler Unix'te standartlaştırılmadan önce geliştirilen bir dizi popüler uygulama bu mimariyi kullanır. Bunlar, hem ticari (Oracle, DB2, Lotus Domino), hem de Sendmail'in ücretsiz, modern sürümleri ve diğer bazı posta sunucuları olan birçok veritabanı sunucusudur.

Aslında çok iş parçacıklı uygulamalar

Uygulama iş parçacıkları veya iş parçacıkları tek bir işlem içinde çalışır. Bir işlemin tüm adres alanı iş parçacıkları arasında paylaşılır. İlk bakışta, bu, herhangi bir özel API'ler olmadan iş parçacıkları arasındaki etkileşimi düzenlemenize izin veriyor gibi görünüyor. Aslında durum böyle değil - birkaç iş parçacığı paylaşılan bir veri yapısı veya sistem kaynağıyla çalışıyorsa ve iş parçacıklarından en az biri bu yapıyı değiştirirse, o zaman bazı noktalarda veriler tutarsız olacaktır.

Bu nedenle, iş parçacıkları etkileşimi düzenlemek için özel araçlar kullanmalıdır. En önemli özellikler karşılıklı dışlama ilkelleridir (muteksler ve okuma-yazma kilitleri). Bu temel öğeleri kullanarak, programcı tutarsız bir durumdayken hiçbir iş parçacığının paylaşılan kaynaklara erişmemesini sağlayabilir (buna karşılıklı dışlama denir). System V IPC , yalnızca paylaşılan bellek segmentinde bulunan yapılar paylaşılır. Olağan şekilde tahsis edilen olağan değişkenler ve dinamik veri yapıları, süreçlerin her birine özeldir). Paylaşılan veri erişim hatalarını - yarış hatalarını - testlerde tespit etmek çok zordur.

  • 1. nokta nedeniyle uygulama geliştirme ve hata ayıklamanın yüksek maliyeti.
  • Düşük güvenilirlik. Arabellek taşmaları veya işaretçi hataları gibi veri yapılarının yok edilmesi, bir işlemdeki tüm iş parçacıklarını etkiler ve genellikle tüm işlemin çökmesine neden olur. İş parçacıklarının birinde sıfıra bölme gibi diğer önemli hatalar da genellikle işlemdeki tüm iş parçacıklarının çökmesine neden olur.
  • Düşük güvenlik. Tüm uygulama iş parçacıkları aynı işlemde, yani aynı kullanıcı adına ve aynı erişim haklarıyla yürütülür. Minimum gerekli ayrıcalıklar ilkesini uygulamak imkansızdır, işlemin, uygulamanın tüm iş parçacıklarının gerektirdiği tüm işlemleri gerçekleştirebilen bir kullanıcı adına yürütülmesi gerekir.
  • Bir iş parçacığı oluşturmak hala oldukça pahalı bir işlemdir. Her iş parçacığının, varsayılan olarak 32 bit mimarilerde 1 megabayt RAM ve 64 bit mimarilerde 2 megabayt RAM ve diğer bazı kaynakları kaplayan kendi yığını olmalıdır. Bu nedenle, bu mimari tüm uygulamalar için uygun değildir.
  • Uygulamayı çoklu bilgisayar kompleksinde yürütmenin imkansızlığı. Paylaşılan dosyaların bellek eşlemesi gibi önceki bölümde bahsedilen teknikler, çok iş parçacıklı bir program için geçerli değildir.
  • Genel olarak, çok iş parçacıklı uygulamaların, paylaşılan bellek kullanan çok işlemli uygulamalarla hemen hemen aynı avantaj ve dezavantajlara sahip olduğu söylenebilir.

    Bununla birlikte, çok iş parçacıklı bir uygulamanın yürütme maliyeti daha düşüktür ve böyle bir uygulamanın geliştirilmesi bazı açılardan paylaşılan bellekli bir uygulamanınkinden daha kolaydır. Bu nedenle, son yıllarda çok iş parçacıklı uygulamalar giderek daha popüler hale geldi.