12 - Eş Zamanlılık, Paralellik ve Asenkron Programlama
2026
Bir program aynı anda birden fazla işle ilgilenmek zorunda kalabilir.
Örneğin:
Bu derste bu tür durumlar için kullanılan üç temel yaklaşımı göreceğiz.
| Yaklaşım | Temel fikir | Python’daki araç |
|---|---|---|
| Eş zamanlılık | Birden fazla işi aynı zaman aralığında yönetmek | threading |
| Paralellik | Birden fazla işi gerçekten aynı anda çalıştırmak | multiprocessing |
| Asenkron programlama | Bekleme sırasında başka görevlere geçebilmek | asyncio |
Bu üç yaklaşım aynı şey değildir.
En önemli soru şudur:
Program daha çok bekliyor mu, yoksa daha çok hesaplama mı yapıyor?
Bazı işler çoğunlukla bekleme içerir.
Örnek:
Bazı işler ise işlemciyi yoğun kullanır.
Örnek:
| Durum | Uygun yaklaşım |
|---|---|
| Program çoğunlukla dosya, ağ veya kullanıcı bekliyorsa | threading veya asyncio |
| Program yoğun hesaplama yapıyorsa | multiprocessing |
| Çok sayıda ağ isteği veya bekleyen görev varsa | asyncio |
| Aynı veriye birden fazla thread erişiyorsa | threading + dikkatli veri koruma |
Python’da threading her zaman gerçek paralellik sağlamaz.
CPU-bound işler için çoğu zaman multiprocessing daha uygundur.
Thread, aynı program içinde çalışan ayrı bir yürütme akışı gibi düşünülebilir.
Bir program birden fazla thread başlatabilir.
Bu özellikle bekleme içeren işlerde yararlı olabilir.
Örnek:
import threading
import time
def yaz():
for i in range(5):
print("Yazma işlemi")
time.sleep(0.2)
def oku():
for i in range(5):
print("Okuma işlemi")
time.sleep(0.2)
thread1 = threading.Thread(target=yaz)
thread2 = threading.Thread(target=oku)
thread1.start()
thread2.start()
thread1.join()
thread2.join()Bu örnekte iki farklı thread başlatılır.
Çıktıların sırası her çalıştırmada aynı olmak zorunda değildir.
start() ve join()start() thread’i başlatır.
join() ise thread’in tamamlanmasını bekler.
join() kullanılmazsa ana program thread’i başlattıktan sonra sonraki satırlara devam eder.
Thread’in bitmesini beklemek istiyorsak join() kullanırız.
Önceki örneği değiştirin.
Üç thread oluşturun:
"Dosya okunuyor""Veri işleniyor""Sonuç kaydediliyor"Her thread kendi mesajını 5 kez yazdırsın.
Çıktı sırasını gözlemleyin.
Birden fazla thread aynı veriyi aynı anda değiştirmeye çalışırsa beklenmeyen sonuçlar ortaya çıkabilir.
Bu duruma race condition denir.
Örnek olarak ortak bir sayaç düşünelim.
Sayaç değerini 10 thread aynı anda artırmaya çalışırsa bazı artırma işlemleri kaybolabilir.
import threading
import time
class Counter:
def __init__(self):
self.counter = 0
def increment(self):
current_value = self.counter
time.sleep(0.00001)
self.counter = current_value + 1
counter = Counter()
def worker():
for _ in range(10000):
counter.increment()Burada increment() metodu ortak sayaç değerini artırır.
Fakat bu işlem aynı anda birden fazla thread tarafından çağrılabilir.
threads = []
for _ in range(10):
thread = threading.Thread(target=worker)
threads.append(thread)
for thread in threads:
thread.start()
for thread in threads:
thread.join()
expected_value = len(threads) * 10000
print(f"Son sayaç değeri: {counter.counter}")
print(f"Beklenen sayaç değeri: {expected_value}")Beklenen değer:
Fakat program her zaman bu değeri üretmeyebilir.
Sayaç artırma işlemi tek adım gibi görünür.
Fakat aslında birkaç adımdan oluşur:
İki thread aynı eski değeri okursa, artırmalardan biri kaybolabilir.
import threading
import time
class Counter:
def __init__(self):
self.counter = 0
self.lock = threading.Lock()
def increment(self):
with self.lock:
current_value = self.counter
time.sleep(0.00001)
self.counter = current_value + 1with self.lock: bloğuna aynı anda yalnızca bir thread girebilir.
Böylece sayaç değeri okunurken ve güncellenirken başka bir thread araya giremez.
Lock, paylaşılan veriyi korumak için kullanılır.
Ancak her yere lock koymak doğru bir yaklaşım değildir.
Dikkat edilmesi gerekenler:
with lock: kullanımını tercih edin.Bir kod parçası aynı anda birden fazla thread tarafından kullanıldığında veri tutarlılığını koruyabiliyorsa thread-safe kabul edilir.
Örneğin:
Bu derste temel çözüm olarak Lock kullanımını gördük.
Multiprocessing, birden fazla process kullanarak çalışır.
Thread’lerden farklı olarak process’ler ayrı bellek alanlarına sahiptir.
Bu yapı özellikle CPU-bound işler için uygundur.
Örnek:
Her process ayrı çalıştığı için birden fazla CPU çekirdeğinden yararlanmak mümkün olabilir.
Aşağıdaki örnekte 1’den 100’e kadar sayıların kareleri hesaplanır.
Her process 10 sayının karesini hesaplar.
from multiprocessing import Process
def calculate_squares(start, end):
result = []
for i in range(start, end + 1):
result.append(i * i)
print(f"{start}-{end} arası kareler: {result}")
if __name__ == "__main__":
processes = []
for i in range(1, 101, 10):
p = Process(target=calculate_squares, args=(i, i + 9))
processes.append(p)
p.start()
for p in processes:
p.join()
print("Tüm işlemler bitti!")if __name__ == "__main__" Notumultiprocessing kullanırken ana program bloğu şu şekilde korunmalıdır:
Bu kullanım özellikle Windows ve macOS ortamlarında önemlidir.
Yeni process oluşturulurken Python dosyası yeniden yüklenebilir.
Ana kod bu blok içine alınmazsa program beklenmeyen şekilde tekrar çalışabilir.
Birden fazla process aynı anda çalıştığında çıktıların sırası garanti edilmez.
Örneğin:
Bu durum hata olmak zorunda değildir.
Process’lerin çalışma sırası işletim sistemi tarafından belirlenir.
asyncio, bekleme içeren işleri asenkron olarak yönetmek için kullanılır.
Özellikle şu tür işler için uygundur:
Burada amaç CPU’yu daha fazla kullanmak değildir.
Amaç, bekleme sırasında programın başka görevleri ilerletebilmesidir.
asyncio programlarında görevleri event loop yönetir.
Event loop basitçe şunu yapar:
Bu derste asenkron programları çalıştırmak için şu yapıyı kullanacağız:
async ve awaitAsenkron fonksiyonlar async def ile tanımlanır.
Bekleme gerektiren asenkron işlemler await ile beklenir.
await sırasında event loop başka görevleri çalıştırabilir.
time.sleep() ve asyncio.sleep() FarkıNormal bekleme:
Bu kullanım mevcut akışı durdurur.
Asenkron bekleme:
Bu kullanım event loop’a başka görevleri çalıştırma fırsatı verir.
import asyncio
async def veri_cek(kaynak, sure):
print(f"{kaynak} için istek gönderildi")
await asyncio.sleep(sure)
print(f"{kaynak} yanıt verdi")
return f"{kaynak} sonucu"
async def main():
sonuclar = await asyncio.gather(
veri_cek("API-1", 2),
veri_cek("API-2", 1),
veri_cek("API-3", 3)
)
print(sonuclar)
asyncio.run(main())Bu örnekte üç görev birlikte başlatılır.
Hangisinin cevabı önce gelirse o görev önce tamamlanır.
asyncio.gather()asyncio.gather() birden fazla asenkron görevi birlikte çalıştırmak için kullanılır.
Bu yapı, verilen görevlerin tamamlanmasını bekler.
Bekleme sırasında görevler birbirini gereksiz yere bekletmez.
asyncio, CPU-bound işler için tek başına uygun değildir.
Örneğin:
Bu tür işler event loop’u bloklayabilir.
Bu durumda multiprocessing daha uygun olabilir.
| Senaryo | Uygun yaklaşım | Neden |
|---|---|---|
| Dosya okurken programın beklememesi | threading |
I/O-bound iş |
| Büyük bir hesaplamayı parçalara bölmek | multiprocessing |
CPU-bound iş |
| 100 farklı API adresinden veri çekmek | asyncio |
Çok sayıda bekleyen ağ isteği |
| Ortak sayacı birden fazla thread ile artırmak | threading + Lock |
Paylaşılan veri korunmalı |
| Arayüz açıkken arka planda indirme yapmak | threading veya asyncio |
Bekleme içeren görev |
Aşağıdaki durumlar için uygun yaklaşımı seçin:
Seçenekler:
threadingmultiprocessingasynciothreading + Lock| Durum | Olası cevap | Gerekçe |
|---|---|---|
| 500 dosyanın okunması | threading |
Dosya okuma bekleme içerebilir |
| Büyük görüntü filtreleme | multiprocessing |
İşlemci yoğun işlem olabilir |
| 100 API adresinden veri çekme | asyncio |
Çok sayıda ağ beklemesi vardır |
| Ortak sayacı artırma | threading + Lock |
Paylaşılan veri korunmalıdır |
| Arka planda dosya indirme | threading veya asyncio |
Bekleme içeren görevdir |
Bazı durumlarda birden fazla doğru cevap olabilir.
Önemli olan seçimin gerekçesini açıklayabilmektir.
Hangisi gerçek paralellik için daha uygundur?
threadingasynciomultiprocessingtime.sleepinputAşağıdaki kodda thread’in başlaması için hangi satır eksiktir?
thread.run()thread.join()thread.start()thread.call()thread.activate()Asenkron bir fonksiyon hangi anahtar kelime ile tanımlanır?
asyncawaityieldlambdadefasyncAşağıdaki üç küçük uygulamayı yazın.
Threading uygulaması
Bir klasördeki birden fazla metin dosyasındaki kelime sayılarını thread kullanarak hesaplayın.
Multiprocessing uygulaması
Büyük bir metni parçalara bölerek her parçadaki kelime sayısını farklı process ile hesaplayın.
Asyncio uygulaması
Farklı kaynaklardan veri çekmeyi temsil eden üç asenkron fonksiyon yazın. Her fonksiyon farklı süre beklesin ve sonucunu döndürsün.
Threading uygulamasında kullanmak için birkaç örnek metin dosyası oluşturabilirsiniz.
from pathlib import Path
klasor = Path("metinler")
klasor.mkdir(exist_ok=True)
icerikler = [
"Python thread kullanımı giriş seviyesi bir konudur.",
"Eş zamanlılık bekleme içeren işlerde faydalı olabilir.",
"Dosya okuma işlemleri I/O-bound örneği olarak düşünülebilir."
]
for index, icerik in enumerate(icerikler, start=1):
dosya = klasor / f"metin_{index}.txt"
dosya.write_text(icerik, encoding="utf-8")Her uygulama için:
Ek soru:
Aynı problemi üç yöntemle çözmeye çalışmak her zaman mantıklı mıdır? Neden?
Bu derste üç temel yaklaşımı gördük:
threading: Bekleme içeren işleri aynı program içinde yönetmekmultiprocessing: CPU-bound işleri farklı process’lerle çalıştırmakasyncio: Çok sayıda bekleyen görevi event loop ile yönetmekTemel karar noktası:
Program daha çok bekliyor mu, yoksa daha çok hesaplama mı yapıyor?
Bu ayrım, kullanılacak yöntemi belirlemede en önemli ölçüttür.