Python'da async/await yapısı, bir programın G/İ (giriş/çıkış) beklerken donmasını önleyen; bekleme sırasında aynı iş parçacığında başka işler yapılmasına izin veren bir eşzamansız programlama modelidir. Bu modelin merkezinde asyncio kütüphanesi ve event loop (olay döngüsü) bulunur: event loop, hangi coroutine'in ne zaman çalışacağına karar veren tek iş parçacıklı bir zamanlayıcıdır. Python asyncio mantığını anlamak için önce coroutine kavramını, sonra await'in ne yaptığını, ardından event loop'un coroutine'leri nasıl sıraya koyup çalıştırdığını adım adım görmek gerekir.
Bu yazıda senkron Python bilgisine sahip ama asenkron tarafa yeni geçen biri için, coroutine tanımından asyncio.gather ile eşzamanlı bekleme örneklerine, sık yapılan hatalardan asyncio ile threading arasındaki kavramsal farka kadar pratik bir yol izlenecek.
Coroutine Nedir? async def ile Normal Fonksiyon Farkı
Normal bir Python fonksiyonu çağrıldığı anda gövdesi baştan sona çalışır ve sonuç doğrudan geri döner. async def ile tanımlanan bir fonksiyon ise çağrıldığında gövdesini hemen çalıştırmaz; bunun yerine bir coroutine nesnesi döndürür. Bu nesne, "bu iş henüz yapılmadı ama nasıl yapılacağı hazır" anlamına gelen bir plan gibidir. Coroutine'in içindeki kod satırları, ancak biri o coroutine'i çalıştırmaya karar verdiğinde işlemeye başlar.
Bu farkı görmenin en net yolu, bir coroutine'i doğrudan print etmektir. Aşağıdaki örnekte fonksiyon çağrılmış gibi görünse de içindeki print satırı hiç çalışmaz:
import asyncio
async def merhaba():
print("Fonksiyon gövdesi çalıştı")
return "tamam"
sonuc = merhaba()
print(sonuc) #
print(type(sonuc)) #
Görüldüğü gibi merhaba() çağrısı "Fonksiyon gövdesi çalıştı" yazısını hiçbir zaman ekrana basmaz; çünkü çağrı, kodu çalıştırmak yerine sadece bir coroutine nesnesi üretmiştir. Bu nesne, yalnızca await ile beklendiğinde ya da bir event loop tarafından çizelgeye alınıp çalıştırıldığında gerçekten iş yapar. Coroutine'i çalıştırmadan sadece oluşturmak, yaygın bir kafa karışıklığı kaynağıdır ve asyncio ile ilk kez çalışan herkesin karşılaştığı bir aşamadır.
Bu ayrımı sağlam bir zemine oturtmak isteyenler için birebir Python dersleri kapsamında coroutine, event loop ve eşzamansız akış konuları uygulamalı örneklerle adım adım işlenir; böylece kavram yalnızca teoride kalmaz, gerçek kod üzerinde pekişir.
await Ne İşe Yarar? Unutulan await Hatası
await anahtar kelimesi, bir coroutine nesnesinin fiilen çalıştırılmasını ve tamamlanmasını beklemek, ardından ürettiği sonucu almak için kullanılır. Bir coroutine'i await etmeden çağırmak, o işi "başlatmak" değil, sadece "planlamak" anlamına gelir; iş fiilen yapılmaz. Bu yüzden async def ile yazılmış bir fonksiyonu çağırırken önüne await koymayı unutmak, kodun sessizce yanlış çalışmasına yol açan en sık karşılaşılan hatalardan biridir.
Aşağıdaki örnek, await unutulduğunda ortaya çıkan durumu gösterir:
import asyncio
async def veri_getir():
await asyncio.sleep(1)
return 42
async def main():
sonuc = veri_getir() # await unutuldu
print(sonuc) # coroutine nesnesi, 42 değil
asyncio.run(main())
Burada sonuc değişkeni beklenen 42 değerini değil, henüz çalıştırılmamış bir coroutine nesnesini tutar. Kod hata fırlatmadan devam edebilir; ancak sonraki satırlarda bu değer üzerinde işlem yapılmaya çalışıldığında beklenmedik tip hataları ya da mantık hataları ortaya çıkar. Python, bu tür durumları fark ettiğinde genellikle bir uyarı üretir: bir coroutine oluşturulmuş ama hiçbir zaman beklenmemiş (awaited) olduğunda, "coroutine never awaited" mantığında bir uyarı mesajı görülür. Bu uyarı, coroutine'in çöp toplayıcı tarafından temizlenirken hâlâ çalıştırılmamış olduğunun fark edilmesinden kaynaklanır; yani Python size "bu işi planladın ama hiç çalıştırmadın" demeye çalışır.
Bu durumun pratik sonucu şudur: async def ile tanımlanmış bir fonksiyonu çağırdığınızda, o çağrının bir sonuç değil bir coroutine döndürdüğünü her zaman hatırlamak gerekir. Sonucu kullanmak istiyorsanız ya await ile bekleyin ya da bu coroutine'i asyncio.run, asyncio.gather gibi bir mekanizmaya verin. Aksi hâlde kod "çalışıyormuş gibi görünen ama aslında hiçbir şey yapmayan" bir hâle bürünür; bu da özellikle uzun fonksiyon zincirlerinde fark edilmesi zor hatalara yol açar.
asyncio.run() ile Coroutine Çalıştırma: Adım Adım

asyncio.run(), bir asyncio programını en üst seviyeden başlatmanın standart yoludur. Bu fonksiyon, elle event loop oluşturup yönetme derdini ortadan kaldırır; kendisine verilen coroutine'i çalıştırmak için gereken tüm alt yapıyı kurar ve iş bitince temizler. asyncio.run() çağrıldığında arka planda sırasıyla şunlar gerçekleşir:
asyncio.run(main())çağrılır ve parametre olarak verilenmain()coroutine nesnesi teslim alınır.- Bu çağrı için yeni bir event loop oluşturulur; bu loop, o çalıştırma oturumuna özeldir.
- Verilen coroutine, oluşturulan bu loop üzerinde çalıştırılmaya başlanır; coroutine içindeki her
awaitnoktasında loop, bekleme süresince başka görevlere geçebilir. - Coroutine tamamlandığında ürettiği sonuç,
asyncio.run()çağrısının dönüş değeri olarak geri verilir. - İş bittikten sonra event loop kapatılır ve kaynaklar serbest bırakılır; loop bir daha bu oturum için kullanılmaz.
Kullanımı basit bir örnekle şu şekilde görünür:
import asyncio
async def main():
print("Başladı")
await asyncio.sleep(1)
print("Bitti")
return "sonuç hazır"
sonuc = asyncio.run(main())
print(sonuc)
Burada dikkat edilmesi gereken önemli bir kural var: asyncio.run() yalnızca programın en üst seviyesinden, tek bir kez çağrılmalıdır. Zaten çalışmakta olan bir event loop'un içinden tekrar asyncio.run() çağırmak, yani iç içe çalıştırmaya kalkışmak, çalışma zamanı hatasına yol açar; çünkü bir event loop aynı anda kendi içine yeni bir loop kabul etmez. Bu yüzden asyncio tabanlı bir uygulamada genellikle tek bir giriş noktası olur ve tüm asenkron akış o tek main() coroutine'i üzerinden yürütülür. Kendi seviyenizi ölçmek ve bu tür temel-orta seviye asyncio kavramlarına ne kadar hâkim olduğunuzu görmek isterseniz ücretsiz kodlama bilgisi testi ile hızlı bir öz değerlendirme yapabilirsiniz.
Event Loop Mantığı: Tek İş Parçacığında Sıralı Bekleme
Asyncio dünyasında en çok kafa karıştıran kavram muhtemelen event loop'un kendisidir. Aslında mantığı, aynı anda tek bir masada çalışan ama birden fazla işi takip eden bir kişiyi düşünmekle anlaşılabilir. Bu kişi bir işe başlar, o iş bir bekleme noktasına geldiğinde (örneğin bir belgenin fotokopisinin çekilmesini beklerken) elini boş bırakmaz, o sırada masadaki başka bir işe geçer. Fotokopi hazır olduğunda ilk işe geri döner. Event loop tam olarak bunu yapar: tek bir iş parçacığı üzerinde, birden fazla coroutine'i sırayla ele alır ve biri await ile beklemeye geçtiğinde kontrolü diğerine devreder.
Burada altı çizilmesi gereken nokta şudur: aynı anda çalışan tek bir kod parçası vardır. Gerçek anlamda paralel işlem yoktur; olan şey, bekleme anlarında sıranın başka bir göreve geçmesidir. Bir coroutine await asyncio.sleep(2) dediğinde, event loop o coroutine'i "2 saniye sonra tekrar kontrol et" diyerek bir kenara bırakır ve bu süre boyunca boş beklemek yerine sırası gelen başka bir coroutine'i çalıştırır. İki coroutine de aynı işlemci çekirdeğinde, aynı iş parçacığında yürütülür; sadece hangi anda hangisinin çalıştığı değişir.
Bu yapının değeri, özellikle I/O bekleme süreleri için ortaya çıkar. Bir dosya okuma, ağ isteği ya da veritabanı sorgusu gibi işlemler CPU'yu meşgul etmez, sadece dış kaynaktan cevap gelmesini bekler. Senkron bir programda bu bekleme süresi boşa geçer; program başka hiçbir şey yapamaz. Asyncio ile bu bekleme süresi içinde event loop başka coroutine'lere zaman ayırabilir. Burada dikkat edilmesi gereken şey, işlemin daha hızlı tamamlandığı değil, bekleme sürelerinin boşa harcanmadığıdır. Yoğun matematiksel hesaplama gibi CPU'yu sürekli meşgul eden işlerde bu avantaj aynı şekilde ortaya çıkmaz, çünkü ortada bir "bekleme" anı yoktur, devredilecek bir boşluk oluşmaz.
Özetle event loop, tek bir iş parçacığında çalışan görevler arasında akıllı bir sıra yönetimi yapan mekanizmadır. Görevler birbirini kesmez, birbirinin üzerine binmez; sadece boş kalan anları paylaşırlar. Bu mantığı kavramak, asyncio ile yazılan kodun neden bazen beklenen kadar "hızlı" görünmediğini, bazen de I/O ağırlıklı senaryolarda gözle görülür bir fark yarattığını anlamanın anahtarıdır.
Senkron ve Asenkron Kodun Karşılaştırması: İki Kısa Örnek

Kavramı somutlaştırmak için aynı işi yapan iki farklı kod parçasına bakalım. İlk örnek, iki "işlem" fonksiyonunu sırayla çağıran klasik senkron bir yapıdır. Her fonksiyon time.sleep ile bir bekleme süresini simüle eder ve program bu süreler boyunca başka hiçbir şey yapamaz:
import time
def islem(ad, sure):
print(f"{ad} basladi")
time.sleep(sure)
print(f"{ad} bitti")
baslangic = time.time()
islem("Islem A", 2)
islem("Islem B", 2)
print("Toplam sure:", time.time() - baslangic)
Bu kodda ikinci işlem, birinci işlem tamamen bitmeden başlayamaz. İki bekleme süresi art arda eklenir, dolayısıyla toplam süre bu iki bekleme süresinin toplamına yakın çıkar. Şimdi aynı mantığı async def ve await ile yazalım:
import asyncio
import time
async def islem(ad, sure):
print(f"{ad} basladi")
await asyncio.sleep(sure)
print(f"{ad} bitti")
async def main():
baslangic = time.time()
await islem("Islem A", 2)
await islem("Islem B", 2)
print("Toplam sure:", time.time() - baslangic)
asyncio.run(main())
Dikkat edilmesi gereken önemli bir nokta var: bu ikinci örnekte de fonksiyonlar sırayla await edildiği için toplam süre yine birbirine eklenir; asenkron yazmak tek başına otomatik bir hızlanma sağlamaz. Asıl fark, bu bekleme sürelerinin eşzamanlı olarak başlatılabilmesi mümkün hale geldiğinde ortaya çıkar; bunu bir sonraki bölümde asyncio.gather ile göreceğiz. Senkron kodda süreler kesinlikle toplanırken, asenkron yapıda görevler doğru kurgulandığında bekleme süreleri örtüşebilir. Bu ayrımı kod yazarak, hatalar üzerinden pratik yaparak pekiştirmek isteyen öğrenciler için Python bilgi seviyesini ölçen kısa test, mevcut seviyeyi görmek açısından pratik bir başlangıç noktası olabilir.
asyncio.gather ile Eşzamanlı Bekleme: Sahte Ağ İsteği Örneği
Asenkron programlamanın gerçek değerini görmek için birden fazla bekleme gerektiren işlemi aynı anda başlatmak gerekir. Bunu göstermenin en kolay yolu, gerçek bir ağ isteği yapmadan onun davranışını taklit eden sahte fonksiyonlar tanımlamaktır. Aşağıdaki örnekte üç "istek" fonksiyonu, her biri farklı bir sürede yanıt veren bir sunucuyu simüle eder:
import asyncio
import time
async def sahte_istek(ad, sure):
print(f"{ad} istegi gonderildi")
await asyncio.sleep(sure)
print(f"{ad} yaniti geldi")
return f"{ad}: tamamlandi"
async def main():
baslangic = time.time()
sonuclar = await asyncio.gather(
sahte_istek("Servis A", 2),
sahte_istek("Servis B", 3),
sahte_istek("Servis C", 1),
)
print(sonuclar)
print("Toplam sure:", time.time() - baslangic)
asyncio.run(main())
Bu örnekte üç coroutine de asyncio.gather içine verildiği anda event loop tarafından hemen başlatılır. Servis A, B ve C bekleme sürelerine aynı anda girer; event loop bu bekleme süreleri boyunca sırayla hangisi hazırsa onun devamına geçer. Sonuç olarak toplam süre, üç bekleme süresinin ayrı ayrı toplanmasından daha kısa olabilir, çünkü bekleme süreleri art arda değil örtüşerek geçer. Burada dikkat edilmesi gereken ifade "olabilir" kelimesidir; kesin bir kat veya oran vermek yanıltıcı olur, çünkü gerçek sonuç görevlerin sayısına, bekleme sürelerine ve sistemin genel yüküne göre değişir.
asyncio.gather, birden fazla coroutine'i tek bir noktada toplayıp hepsinin bitmesini beklemek istediğinizde kullanılan temel araçlardan biridir. Fonksiyon, verilen coroutine'lerin sonuçlarını çağrıldıkları sırayla bir liste halinde döndürür; bu sayede hangi sonucun hangi isteğe ait olduğunu takip etmek kolaylaşır. Gerçek dünyada bu yapı, aynı anda birden fazla API'ye istek atmak, birden fazla dosyayı okumak ya da birden fazla veritabanı sorgusunu beklemek gibi senaryolarda karşımıza çıkar; sahte asyncio.sleep çağrıları burada yalnızca öğretici bir yer tutucudur.
Sık Yapılan Hatalar: await Unutma, Bloklayan Kod ve Event Loop Kilitlenmesi
Asenkron koda yeni geçen bir Python geliştiricisinin karşılaştığı sorunların büyük bölümü, aslında dilin kendisinden değil, senkron alışkanlıkların asenkron bağlama taşınmasından kaynaklanır. Aşağıdaki hatalar, kod çalışıyor gibi görünse bile beklenen davranışı vermemesine yol açan en yaygın durumlardır:
- await unutmak: Bir coroutine fonksiyonunu çağırırken await koymayı unutmak, fonksiyonun içeriğinin hiç çalıştırılmadığı anlamına gelir; sonuç olarak elinizde çalışan bir görev değil, yalnızca bir coroutine nesnesi kalır.
- Coroutine nesnesini hiç çalıştırmadan bırakmak: async def ile tanımlanan bir fonksiyonu çağırmak onu otomatik olarak çalıştırmaz; bu nesne asyncio.run() ile ya da başka bir coroutine içinde await ile event loop'a teslim edilmezse, işlem tamamen sessizce göz ardı edilir.
- async fonksiyon içinde time.sleep gibi bloklayan senkron çağrı kullanmak: Bu, asenkron kodda en sık görülen ve en yanıltıcı hatalardan biridir çünkü kod hatasız çalışır ama beklenen eşzamanlılığı sağlamaz.
- Bekleme noktası olmadan uzun döngüler yazmak: await içermeyen ağır bir işlem, event loop'un başka görevlere sıra vermesini engelleyerek programın genel akışını yavaşlatabilir.
Bu liste içinde özellikle time.sleep örneği üzerinde durmakta fayda var. asyncio.sleep bir coroutine olduğu için await ile çağrıldığında event loop'a kontrolü geri verir; bu sayede loop, bekleme süresi boyunca başka bekleyen görevleri çalıştırabilir. time.sleep ise senkron bir çağrıdır ve çalıştığı iş parçacığını doğrudan durdurur. Tek iş parçacığında çalışan event loop söz konusu olduğunda bu, yalnızca o coroutine'in değil, o anda bekleyen tüm diğer görevlerin de donması anlamına gelir. Yani programda birden fazla asenkron görev varmış gibi görünse bile, aralarına sızan tek bir time.sleep çağrısı tüm zamanlamayı senkron bir programa geri döndürebilir.
Bu tür hataların çoğu, event loop'un nasıl çalıştığını görsel olarak izlemeden yalnızca kod okuyarak fark edilmesi zor hatalardır; bu yüzden birebir geri bildirim alarak pratik yapmak öğrenme sürecini kısaltır. 1-1 Python özel dersi kapsamında bu tarz hataların canlı örnekler üzerinde adım adım incelenmesi, öğrencinin kendi kodundaki benzer tuzakları daha hızlı fark etmesini sağlar.
Asyncio ve Threading Arasındaki Fark: Ne Zaman Hangisi?
Asyncio ve threading, ikisi de "aynı anda birden fazla iş yapıyormuş gibi görünme" hedefine hizmet eder, ancak bu hedefe tamamen farklı mekanizmalarla ulaşırlar. Asyncio tek bir iş parçacığı üzerinde, görevler arasında kontrollü geçişlerle çalışırken; threading gerçekten birden fazla iş parçacığı açarak işletim sistemi seviyesinde zamanlama yapar. Bu fark, hangi senaryoda hangisinin daha uygun olduğunu belirleyen temel noktadır.
Genel bir kural olarak, bekleme süresinin baskın olduğu I/O ağırlıklı işler (ağ isteği, dosya okuma, veritabanı sorgusu gibi) asyncio'nun doğal olarak rahat çalıştığı senaryolardır çünkü bu bekleme süreleri boyunca event loop başka görevlere geçebilir. Yoğun matematiksel hesaplama gibi gerçek paralellik gerektiren durumlarda ise farklı yaklaşımlar (çoklu iş parçacığı veya çoklu süreç) değerlendirilebilir; bu tercih, işin doğasına göre değişir ve tek bir yöntemin her durumda daha hızlı olduğu şeklinde genellenemez.
| Özellik | Asyncio | Threading |
|---|---|---|
| Çalışma modeli | Tek iş parçacığı, event loop üzerinden sıralı görev geçişi | Birden fazla iş parçacığı, işletim sistemi zamanlayıcısı |
| En uygun olduğu iş türü | I/O bekleme yoğun işler (ağ, dosya, veritabanı) | Bağımsız çalışan, orta düzey paralellik gerektiren işler |
| Kod yazım tarzı | async def, await, coroutine yapıları | Thread oluşturma, kilit (lock) ve senkronizasyon yönetimi |
| Hata ayıklama zorluğu | Bekleme noktalarını ve await zincirini takip etmek gerekir | Yarış durumları (race condition) ve kilitlenmeler takip edilmelidir |
| Paylaşılan veri yönetimi | Tek iş parçacığı olduğu için genelde daha az karmaşık | Ortak veriye erişimde kilit mekanizmaları gerektirir |
Bu iki yaklaşımı birbirine alternatif değil, farklı problemler için farklı araçlar olarak görmek daha sağlıklıdır. Konuyu kod örnekleriyle derinlemesine incelemek isteyenler için Python eşzamansız programlama yazı dizisi içinde bu iki modelin farklı senaryolardaki davranışını karşılaştıran ek içeriklere ulaşmak mümkündür.
Sık Sorulan Sorular
async ve await ne zaman kullanılmalı?
Programın büyük bölümü ağ isteği, dosya işlemi veya veritabanı sorgusu gibi bekleme içeren I/O işlemlerinden oluşuyorsa async ve await kullanmak, bu bekleme sürelerini verimli değerlendirmenin bir yoludur. Yoğun hesaplama gerektiren, bekleme içermeyen işlerde asenkron yapı ek bir kazanç sağlamayabilir.
Event loop tam olarak nedir ve nasıl çalışır?
Event loop, bekleyen coroutine'leri takip eden, hangi görevin ne zaman devam edeceğine karar veren ve tek bir iş parçacığında görevler arasında sırayla geçiş yapan bir zamanlayıcı mekanizmasıdır. Bir görev await ile beklemeye geçtiğinde, kontrolü geri verir ve loop bu sırada başka bir görevi çalıştırabilir.
asyncio.run() fonksiyonu ne işe yarar?
asyncio.run(), bir event loop oluşturur, verilen ana coroutine'i bu loop üzerinde çalıştırır ve iş bitince loop'u düzgün şekilde kapatır. Genellikle bir asenkron programın başlangıç noktasında, en üst seviyede bir kez çağrılır.
Asyncio ile threading arasındaki temel fark nedir?
Asyncio tek bir iş parçacığında kontrollü görev geçişleriyle çalışırken, threading gerçekten birden fazla iş parçacığı açarak işletim sistemi seviyesinde zamanlama yapar. Bu nedenle kod yazım tarzları, hata ayıklama yaklaşımları ve uygun oldukları iş türleri farklılaşır.
await unutulursa ne olur?
await konulmadan çağrılan bir coroutine fonksiyonu çalıştırılmaz; geriye yalnızca bir coroutine nesnesi kalır ve fonksiyonun içindeki kod hiç işletilmez. Bu, sessizce göz ardı edilen bir hata olduğu için fark edilmesi genellikle zaman alır.
async fonksiyon içinde time.sleep kullanmak neden sorun yaratır?
time.sleep senkron bir çağrı olduğu için çalıştığı iş parçacığını tamamen durdurur. Event loop tek bir iş parçacığı üzerinde çalıştığından, bu bekleme sırasında diğer tüm bekleyen görevler de donar; oysa asyncio.sleep kontrolü event loop'a geri vererek bu sorunu ortadan kaldırır.
Asyncio her zaman thread'lerden daha mı hızlıdır?
Hayır, bu şekilde genellenemez. Asyncio, bekleme ağırlıklı I/O işlerinde rahat çalışan bir yaklaşımdır; ancak hangi yöntemin daha uygun olduğu işin türüne, bekleme oranına ve uygulamanın yapısına göre değişir.
Asyncio, async/await ve event loop mantığını kavramak, Python'da eşzamansız programlamanın kapısını aralayan ilk adımdır; ancak bu kavramları gerçek projelerde rahatça kullanabilmek pratikle gelir. Temel senkron Python bilgisini asenkron yapılarla pekiştirmek isteyenler Python yazılım eğitimi içeriğine göz atarak konuyu daha uygulamalı örneklerle derinleştirebilir.