Python'da Asyncio ile Çok Çekirdekli Güçten Yararlanma
Bu, Python Eşzamanlılık sütunu altındaki makalelerimden biridir ve yararlı bulursanız, gerisini buradan okuyabilirsiniz .
giriiş
Bu makalede, eşzamanlı görevlerin tam performansının kilidini açmak için çok çekirdekli bir CPU'da Python asyncio kodunu nasıl çalıştıracağınızı göstereceğim.
Bizim sorunumuz nedir?
asyncio yalnızca bir çekirdek kullanır.
Önceki makalelerde, Python asyncio kullanmanın mekaniklerini ayrıntılı olarak ele aldım. Bu bilgiyle, asyncio'nun, çok iş parçacıklı görev değiştirme sırasında GIL çekişme sürecini atlamak için görev yürütmeyi manuel olarak değiştirerek IO'ya bağlı görevlerin yüksek hızda yürütülmesine izin verdiğini öğrenebilirsiniz.
Teorik olarak, GÇ'ye bağlı görevlerin yürütme süresi, bir G/Ç işleminin başlatılmasından yanıtlanmasına kadar geçen süreye bağlıdır ve CPU performansınıza bağlı değildir. Böylece aynı anda on binlerce IO görevini başlatabiliyor ve bunları hızlı bir şekilde tamamlayabiliyoruz.
Ancak son zamanlarda, on binlerce web sayfasını aynı anda taraması gereken bir program yazıyordum ve asyncio programımın web sayfalarını yinelemeli taramayı kullanan programlardan çok daha verimli olmasına rağmen, beni yine de uzun süre beklettiğini gördüm. Bilgisayarımın tam performansını kullanmalı mıyım? Bu yüzden Görev Yöneticisi'ni açtım ve kontrol ettim:
Başından beri kodumun yalnızca bir CPU çekirdeğinde çalıştığını ve diğer birkaç çekirdeğin boşta olduğunu gördüm. Ağ verilerini almak için G/Ç işlemlerini başlatmanın yanı sıra, bir görevin geri döndükten sonra verileri paketinden çıkarması ve biçimlendirmesi gerekir. İşlemin bu kısmı çok fazla CPU performansı tüketmese de, daha fazla görevden sonra bu CPU'ya bağlı işlemler genel performansı ciddi şekilde etkileyecektir.
Asyncio eşzamanlı görevlerimin birden çok çekirdekte paralel olarak yürütülmesini sağlamak istedim. Bu, bilgisayarımın performansını düşürür mü?
Asyncio'nun temel ilkeleri
Bu bulmacayı çözmek için, temeldeki asyncio uygulamasıyla, olay döngüsüyle başlamalıyız.
Şekilde gösterildiği gibi, asyncio'nun programlar için performans iyileştirmesi G/Ç yoğun görevlerle başlar. IO-yoğun görevler arasında HTTP istekleri, dosya okuma ve yazma, veritabanlarına erişim vb. yer alır. Bu görevlerin en önemli özelliği, CPU'nun engellememesi ve harici verilerin döndürülmesini beklerken çok fazla zaman harcamasıdır; belirli bir sonucu hesaplamak için CPU'nun her zaman meşgul olmasını gerektiren başka bir eşzamanlı görev sınıfından çok farklıdır.
Bir dizi eşzamansız görev oluşturduğumuzda, kod önce bu görevleri bir kuyruğa koyacaktır. Bu noktada, sıradan her seferinde bir görevi alan ve onu yürüten olay döngüsü adı verilen bir iş parçacığı vardır. Görev, wait deyimine ulaşıp beklediğinde (genellikle bir isteğin geri dönüşü için), olay döngüsü sıradan başka bir görevi alır ve onu yürütür. Önceden bekleyen görev bir geri çağırma yoluyla veri alana kadar, olay döngüsü önceki bekleyen göreve geri döner ve kodun geri kalanını yürütmeyi bitirir.
Olay döngüsü iş parçacığı yalnızca bir çekirdek üzerinde yürütüldüğünden, "kodun geri kalanı" CPU zamanını aldığında olay döngüsü bloke olur. Bu kategorideki görevlerin sayısı çok olduğunda, her bir küçük engelleme bölümü, programı bir bütün olarak toplar ve yavaşlatır.
benim çözümüm nedir?
Buradan, asyncio programlarının yavaşladığını biliyoruz çünkü Python kodumuz olay döngüsünü yalnızca bir çekirdek üzerinde yürütür ve IO verilerinin işlenmesi programın yavaşlamasına neden olur. Yürütmek için her CPU çekirdeğinde bir olay döngüsü başlatmanın bir yolu var mı?
asyncio.run
Hepimizin bildiği gibi, Python 3.7'den başlayarak, tüm asyncio kodlarının , aşağıdaki koda alternatif olarak kodu yürütmek için olay döngüsünü çağıran üst düzey bir soyutlama olan yöntem kullanılarak yürütülmesi önerilir :
try:
loop = asyncio.get_event_loop()
loop.run_until_complete(task())
finally:
loop.close()
loop.run_in_executor
Önceki makale, asyncio'nun bir işlem havuzunda kod yürütülmesini paralel hale getirmek ve aynı zamanda ana süreçten her bir alt sürecin sonuçlarını almak için yöntemini kullanmayı açıklamak için gerçek hayattan bir örnek kullandı . Önceki makaleyi okumadıysanız, buradan kontrol edebilirsiniz:
Böylece, çözümümüz ortaya çıkıyor: yöntem aracılığıyla çok çekirdekli yürütmeyi kullanarak birçok eşzamanlı görevi birden çok alt işleme dağıtın loop.run_in_executor
ve ardından asyncio.run
ilgili olay döngüsünü başlatmak ve eşzamanlı kodu yürütmek için her bir alt işlemi çağırın. Aşağıdaki diyagram tüm akışı göstermektedir:
Yeşil kısım, başladığımız alt süreçleri temsil ediyor. Sarı kısım, başlattığımız eşzamanlı görevleri temsil eder.
Başlamadan önce hazırlık
Görev uygulamasının simülasyonu
Sorunu çözmeden önce, başlamadan önce hazırlanmamız gerekir. Bu örnekte, hedef web sitesi için çok can sıkıcı olacağından web içeriğini taramak için gerçek kod yazamıyoruz, bu nedenle gerçek görevimizi kodla simüle edeceğiz:
Kodun gösterdiği gibi, ilk önce asyncio.sleep
IO görevinin rastgele zamanda dönüşünü simüle etmek için ve veriler döndürüldükten sonra CPU işlemeyi simüle etmek için yinelemeli bir toplamı kullanırız.
Geleneksel kodun etkisi
Ardından, bir ana yöntemde 10.000 eşzamanlı görevi başlatma şeklindeki geleneksel yaklaşımı benimsiyoruz ve bu eşzamanlı görevler grubu tarafından tüketilen süreyi izliyoruz:
Şekilde görüldüğü gibi, asyncio görevlerini tek bir çekirdekle yürütmek daha uzun zaman alıyor.
kod uygulaması
Ardından, çok çekirdekli asyncio kodunu akış şemasına göre uygulayalım ve performansın iyileşip iyileşmediğini görelim.
Kodun genel yapısını tasarlama
İlk olarak, bir mimar olarak, genel betik yapısını, hangi yöntemlerin gerekli olduğunu ve her yöntemin hangi görevleri yerine getirmesi gerektiğini tanımlamamız gerekiyor:
Her yöntemin özel uygulaması
Ardından, her yöntemi adım adım uygulayalım.
Yöntem, query_concurrently
belirtilen görev grubunu aynı anda başlatacak ve sonuçları şu yöntemle alacaktır asyncio.gather
:
Yöntem run_batch_tasks
, doğrudan alt süreçte başlatıldığı için zaman uyumsuz bir yöntem değildir:
Son olarak, bizim yöntemimiz var main
. Bu yöntem , yöntemin işlem havuzunda yürütülmesini loop.run_in_executor
sağlamak ve alt işlem yürütmesinin sonuçlarını bir listede birleştirmek için yöntemi çağırır :run_batch_tasks
Çok işlemli bir komut dosyası yazdığımızdan, if __name__ == “__main__”
ana işlemde ana yöntemi başlatmak için kullanmamız gerekiyor:
Kodu çalıştırın ve sonuçları görün
Ardından, betiği başlatıyoruz ve görev yöneticisindeki her bir çekirdeğin yüküne bakıyoruz:
Gördüğünüz gibi, tüm CPU çekirdekleri kullanılıyor.
Son olarak, kod yürütme süresini gözlemliyoruz ve çok iş parçacıklı asyncio kodunun gerçekten de kod yürütmeyi birkaç kat hızlandırdığını onaylıyoruz! Görev tamamlandı!
Çözüm
Bu makalede, asyncio'nun IO-yoğun görevleri aynı anda yürütebildiğini ancak büyük eşzamanlı görevleri çalıştırırken neden beklenenden daha uzun sürdüğünü açıkladım.
Bunun nedeni, asyncio kodunun geleneksel uygulama şemasında, olay döngüsünün yalnızca bir çekirdekte görevleri yürütebilmesi ve diğer çekirdeklerin boşta durumda olmasıdır.
Bu nedenle, eşzamanlı görevleri paralel olarak yürütmek için birden çok çekirdekteki her olay döngüsünü ayrı ayrı çağırmanız için bir çözüm uyguladım. Ve son olarak, kodun performansını önemli ölçüde iyileştirdi.
Yeteneğimin sınırlı olması nedeniyle, bu makaledeki çözümde kaçınılmaz olarak kusurlar var. Yorumlarınızı ve tartışmalarınızı bekliyorum. Sizin için aktif olarak cevap vereceğim.