23 puan yazan darjeeling 2025-11-16 | 12 yorum | WhatsApp'ta paylaş

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 def ile tanımlanan asenkron fonksiyonlardır. await anahtar 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.gather veya asyncio.TaskGroup kullanarak 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 await etme zahmeti vardır.
  • asyncio.TaskGroup (Python 3.11+):
    • "Structured concurrency" için modern alternatiftir.
    • Task grubunu async with sö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 ve await etmeyin.
    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.Semaphore kullanı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 await edilmediğinde iş hiç çalışmayabilir ve sessizce başarısız olabilir. flake8-async gibi linter'larla tespit edilebilir.
  • Uygun olmayan kaynak yönetimi: Dosya, DB bağlantısı vb. kaynakları try...finally olmadan kullanmak kaynak sızıntısına yol açabilir. async with kullanan 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.

  • await edilmeyen 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: asyncio uygulamaları 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.

https://youtu.be/wGDOwNW6lVk

12 yorum

 
savvykang 2025-11-18

Python gerçekten harika bir dil ama asenkron arayüzü sanki kötü tasarlanmış bir özellik gibi görünüyor

 
ceruns 2025-11-17
  1. maddede eager_start=True eksik kalmış. create_task weakref oluşturduğu için, bu kod hiç çalıştırılmayacak bir task’a dönüşebilir....
 
tested 2025-11-17

> 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

 
kandk 2025-11-17

Sonuç: Python asenkron arayüzü hâlâ sezgisel değil.

 
bungker 2025-11-17

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.

 
euphcat 2025-11-17

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.

 
vwjdalsgkv 2025-11-17

JIT derleme olup olmaması düşünüldüğünden daha büyük fark yaratıyor. V8 oldukça iyi optimize edilmiş.

 
euphcat 2025-11-16

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 await yazacak ş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 :)

 
kunggom 2025-11-16

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 async metot çağrısının önünde await anahtar 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ımda Task<T> nesnesinin await sonucunu almak; bazılarının ise epey sonra kullanılacağı için önce sadece Task<T>'yi alıp daha sonra await etmek 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.

 
skageektp 2025-11-17

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 await ile seri çalıştırmak daha iyi olabilir

 
euphcat 2025-11-17

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

 
kunggom 2025-11-17

Öyle gerçekten.
Düzgün bir asenkron kod, özünde çok dikkat gerektiren bir kod gibi görünüyor.