Python'da Asyncio ile Çok Çekirdekli Güçten Yararlanma

May 10 2023
Asyncio ile birden çok CPU çekirdeğini verimli bir şekilde kullanarak Python uygulamanızın performansını artırın
Bu, Python Eşzamanlılık sütunu altındaki makalelerimden biridir ve yararlı bulursanız, gerisini buradan okuyabilirsiniz. Giriş 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.
Fotoğraf Katkısı: Yazar, Canva tarafından oluşturulmuştur.

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:

Sadece bir çekirdeğin yükü vardır. Yazara göre resim

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.

Olay döngüsü nasıl çalışır? Yazara göre resim

Ş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.runHepimizin 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_executorve ardından asyncio.runilgili 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:

Kod nasıl yürütülür. Yazara göre resim

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.sleepIO 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.

Tek çekirdekte uzun sürüyor. Yazara göre resim

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_concurrentlybelirtilen 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_executorsağ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:

Tüm çekirdekler neredeyse kullanılıyor. Yazara göre resim

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ı!

Performans artışının neredeyse üç katı! Yazara göre resim

Çö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.