Async kodlarda yavaşlamanın nedenleri ve çözümleri
(secondb.ai)Async kodlarda yavaşlamanın nedenleri ve çözümleri (teknik özet)
Bu video, Python'da asyncio kodunun senkron koda göre daha yavaş hale gelmesinin yaygın nedenlerini ve bunu çözmek için teknik yöntemleri ele alıyor.
1. Asyncio'nun temel kavramları
- Event Loop: Tüm asenkron uygulamaların çekirdeğidir.
asyncio.run()ile başlar, tek bir thread üzerinde task yürütmeyi yönetir ve zamanlar. - Coroutines:
async defile tanımlanan asenkron fonksiyonlardır.awaitanahtar sözcüğüyle karşılaştığında yürütmeyi duraklatıp denetimi event loop'a geri verebilir. - Tasks: Coroutine'leri sarmalar ve event loop üzerinde eşzamanlı çalışacak şekilde zamanlar.
asyncio.create_task()ile oluşturulur. - Futures: Asenkron bir işin nihai sonucunu temsil eden düşük seviyeli nesnelerdir.
2. Senkron kodun asenkron koda dönüştürülmesi örneği
Mevcut senkron time.sleep() çağrısını asenkron await asyncio.sleep() ile değiştirin, fonksiyonu async def olarak tanımlayın ve ana coroutine'i asyncio.run() ile çalıştırın.
Performans düşüşüne yol açan yaygın hatalar ve çözümleri
Hata 1: Sıralı yürütme (Sequential Execution)
Birbirinden bağımsız task'ları paralel çalıştırmak yerine sırayla await ederseniz, toplam çalışma süresi tüm task'ların sürelerinin toplamı olur.
-
Yanlış örnek (sıralı):
# Her await, önceki iş bitene kadar bekler await get_user_notifications() await get_recent_activity() await get_unread_messages() -
Çözüm (paralel):
asyncio.gatherveyaasyncio.TaskGroupkullanarak bağımsız task'ları aynı anda çalıştırın. Toplam süre, en uzun süren task'ın süresine iner.# Üç iş aynı anda başlatılır await asyncio.gather( get_user_notifications(), get_recent_activity(), get_unread_messages() )
Paralel yürütme araçlarının karşılaştırması
asyncio.gather:- Birden çok coroutine'i aynı anda çalıştırır.
- Dezavantajı: Hata işleme zayıftır. Bir task'ta exception oluşursa, çalışan diğer task'lar iptal edilir.
asyncio.create_task:- Task bazında kontrol ve hata işleme sağlar.
- Arka planda çalıştırma için yararlıdır, ancak birden fazla task'ı tek tek
awaitetme zahmeti vardır.
asyncio.TaskGroup(Python 3.11+):- "Structured concurrency" için modern alternatiftir.
- Task grubunu
async withsözdizimiyle yönetir; bağlamdan çıkıldığında tüm task'ların tamamlanması veya exception işlenmesi garanti edilir.
async with asyncio.TaskGroup() as tg: tg.create_task(some_coro_1()) tg.create_task(some_coro_2()) # 'async with' bloğu bittiğinde tüm task'lar await edilmiş olur
Hata 2: Senkron kütüphane kullanımı
asyncio kodu içinde requests veya pathlib gibi senkron (blocking) kütüphaneler kullanılırsa tüm event loop bloke olur. asyncio.gather içinde kullansanız bile gerçekte sıralı çalışır.
- Çözüm:
aiohttp(requests yerine),aiofiles(dosyalar/pathlib yerine) gibi asenkron (non-blocking) desteği olan özel kütüphaneler kullanın.
Hata 3: CPU-bound işler nedeniyle event loop'un bloke olması
asyncio tek bir thread üzerinde çalıştığı için ağır hesaplamalar (CPU-bound) event loop'u durdurur ve diğer I/O işlerini geciktirir.
- Çözüm: CPU-bound işleri ayrı bir thread pool'a (varsayılan) veya process pool'a taşımak için
loop.run_in_executor()kullanın.loop = asyncio.get_running_loop() # CPU yoğun fonksiyonu ayrı bir thread üzerinde çalıştır await loop.run_in_executor( None, # Varsayılan thread pool'u kullan cpu_bound_function, arg1 )
Hata 4: Kritik olmayan işler yüzünden blokaj
Kullanıcı yanıtıyla ilgisiz logging gibi çekirdek olmayan işleri await etmek, yanıt süresini gereksiz yere uzatır.
- Çözüm: Bu işleri
asyncio.create_task()ile arka plan task'ı olarak ayırın veawaitetmeyin.user_profile = await get_user_profile() # Logging'i await etmeden arka planda çalıştır asyncio.create_task(send_logs_to_external_service()) return user_profile
Hata 5: Çok fazla task oluşturmak
Çok küçük işleri büyük miktarda task'a dönüştürmek, context switching overhead'i yaratarak performansı düşürebilir.
- Çözüm 1: Küçük işleri gruplayarak (batching) birkaç daha büyük task haline getirin.
- Çözüm 2: Aynı anda çalışan azami task sayısını sınırlamak için
asyncio.Semaphorekullanın.# Aynı anda en fazla 10 işe izin ver semaphore = asyncio.Semaphore(10) async with semaphore: await fetch_data()
Diğer hatalar
- "Never Awaited" coroutine'ler: Bir coroutine çağrılıp
awaitedilmediğinde iş hiç çalışmayabilir ve sessizce başarısız olabilir.flake8-asyncgibi linter'larla tespit edilebilir. - Uygun olmayan kaynak yönetimi: Dosya, DB bağlantısı vb. kaynakları
try...finallyolmadan kullanmak kaynak sızıntısına yol açabilir.async withkullanan asenkron context manager'larla çözülebilir.
Hata ayıklama ve eşzamanlılık modeli seçimi
Asyncio debug modu
Varsayılan olarak kapalı olan debug modunu etkinleştirmek (asyncio.run(debug=True)), aşağıdaki sorunların tespitine yardımcı olur.
awaitedilmeyen coroutine'ler (RuntimeWarning).- Yanlış thread'den çağrılan asenkron API'ler.
- Çalışma süresi 100 ms'yi aşan callback'ler.
- Yavaş I/O selector işlemleri.
Diğer hata ayıklama araçları
- Scalene: CPU ve bellek profiler'ı.
- aio-monitor:
asynciouygulamaları için izleme ve CLI. - pdb: Python'un yerleşik debugger'ı.
- py-stack: Çalışan Python process'inin stack trace çıktısını vererek blokaj noktalarının tespitine yardımcı olur.
Eşzamanlılık modeli seçme rehberi
- Asyncio (tek thread): Gecikmesi yüksek çok sayıdaki I/O-bound iş için idealdir (ör. ağ istekleri, dosya I/O'su).
- Threads (çoklu thread): Paylaşılan verilere erişim gerektiren I/O-bound işler için kullanılır. GIL (Global Interpreter Lock) nedeniyle gerçek paralellik sağlamasa da, I/O beklerken diğer thread'ler çalışabilir.
- Processes (çoklu process): CPU-bound işler için kullanılır (ör. görüntü işleme, ağır hesaplamalar). Birden fazla CPU çekirdeğini kullanarak gerçek paralellik sağlar, ancak bellek ve iletişim overhead'i yüksektir.
12 yorum
Python gerçekten harika bir dil ama asenkron arayüzü sanki kötü tasarlanmış bir özellik gibi görünüyor
eager_start=Trueeksik kalmış.create_taskweakref oluşturduğu için, bu kod hiç çalıştırılmayacak bir task’a dönüşebilir....> https://rosettalens.com/s/ko/python-to-node
Bu kişi de Python async yüzünden Node.js'e geçtiğini söylüyordu
Sonuç: Python asenkron arayüzü hâlâ sezgisel değil.
Aslında Python asenkronisini optimize edecek kadar büyük bir projeyse, performans ve kararlılık açısından bunu başka bir dille yazmak çok daha iyidir.
Derlenmiş bir dile geçilmeyecekse, performans farkı çok büyük olur mu? Çoklu iş parçacığında GIL’in varlığı nedeniyle büyük fark oluşur elbette, ama sonuçta event loop’un çalıştığı asenkron bir yapıysa dile göre ne tür farklar ortaya çıktığını merak ediyorum.
JIT derleme olup olmaması düşünüldüğünden daha büyük fark yaratıyor. V8 oldukça iyi optimize edilmiş.
Kaynak videoyu izlemedim ama 4. hata için verilen çözüm kodu yanlış.
create_task()tarafından döndürülen görev örneği en az bir değişkene atanmalı ve bu değişken görev tamamlanana kadar yaşamaya devam etmelidir. Aksi halde coroutine çalışırken görev örneğinin garbage collection tarafından toplanma riski vardır.Yukarıdaki gibi görevi oluşturan fonksiyon hemen sona erecekse, görev örneğini döndürmek, global bir değişkene atamak ya da instance değişkenine atamak gibi yöntemler kullanılmalıdır.
P.S)
Dönüş değerine özellikle ihtiyaç olmasa ve coroutine'in kısa sürede biteceğinden emin olsanız bile, görev örneği için er ya da geç bir
awaityazacak şekilde kurgulamak iyi olur. Bunu istemiyorsanız, görev olarak çalışacak her coroutine'e sıkı exception handling ekleyip log mesajlarını eksiksiz üretecek bir yapı kurmanız gerekir. Aksi halde görev ne kadar büyük bir sorun çıkarırsa çıkarsın, Exception işlenmeden sessizce başarısız olma durumu ortaya çıkabilir.Geçimimi sağlamak için geliştirdiğim/yönettiğim bir projede onlarca modülün her biri
while self.ok(): cmd = await self.cmd_queue.get(); await self.process(cmd);şeklinde birer görev oluşturup sürekli çalıştıran bir desen tasarlamıştım. Exception handling deseni oturana kadar, her sorun patladığında benim mentalim de onunla birlikte dağılıyordu; gerçekten ender yaşanan bir tecrübeydi :)Async/Await kalıbının öncüsü(?) sayılabilecek C# kullanan bir şirkette çalışan biri olarak ben bile, 1 numaralı hatadaki gibi
await'leri basitçe art arda sıralayan türden hatalı kodlara epey sık rastlıyorum.Böyle kodları görünce, ortak nokta olarak insanların
asyncmetot çağrısının önündeawaitanahtar sözcüğünü kullanmak gerektiğini bildiği ama bunun ötesinde asenkron çalışma sırası üzerine pek düşünmediği için böyle kodların ortaya çıktığı hissine kapılıyorum.Birden fazla
awaitçıktığında, bazılarının sonucu hemen altta kullanılacağı için o kısımdaTask<T>nesnesininawaitsonucunu almak; bazılarının ise epey sonra kullanılacağı için önce sadeceTask<T>'yi alıp daha sonraawaitetmek gibi, asenkron akışı düşünerek kod yazmak sonuçta o ölçüde kafa yormayı gerektiren bir iş.En azından ben, asenkron olarak tanımlanmış metotlarda işlem akışını düşünerek bu şekilde kod yazıyorum; ama bazen bakımını devraldığım, şirketten ayrılmış birinin mevcut koduna bakınca, “Ben aslında sadece basit bir senkron kod yazmak istiyorum ama arada kullanmam gereken metotlar yalnızca asenkron tipte olduğu için mecburen böyle yazıyorum” duygusu veren durumlar da oluyor.
Eğer 1 numara her zaman bağımsızsa bunu o şekilde yapmak iyi olur,
ama kodu değiştirince bağımsız olmaktan çıkarsa o fonksiyonu kullanan her yeri tek tek gözden geçirip düzeltmek gerekecek gibi görünüyor.
İşlem çok uzun sürmüyorsa, kod yönetimi açısından
awaitile seri çalıştırmak daha iyi olabilirBence buna, "multithreading'deki overhead yükü fazla olduğu için, ikinci bir seçenek olarak tek bir thread'i parçalayıp paralel işlemi çözmek" fikriyle yaklaşmak gerekiyor. Bu yüzden de, temelde multithreading'e kıyasla bazı durumlarda daha fazla özen gerektirmesi gayet doğru gibi görünüyor.
Öyle gerçekten.
Düzgün bir asenkron kod, özünde çok dikkat gerektiren bir kod gibi görünüyor.