Python'da Eşzamanlılık (Concurrency) — Kapsamlı Rehber
İçindekiler
- Giriş: Eşzamanlılık (Concurrency) ve Paralellik (Parallelism)
- Global Interpreter Lock (GIL)
threadingModülümultiprocessingModülüasyncioModülüconcurrent.futuresModülü- Senkronizasyon Araçları
- Kuyruklar ve Görevler Arası İletişim
- Doğru Aracı Seçmek
- Yaygın Hatalar ve İyi Uygulamalar
- İleri Düzey Konular
- Özet Tablo
1. Giriş: Eşzamanlılık (Concurrency) ve Paralellik (Parallelism)
Eşzamanlılık (concurrency), birden fazla işi aynı anda yönetmekle ilgilidir — programı, birden fazla görevin tam olarak aynı anda çalışmasa bile ilerleme kaydedebileceği şekilde yapılandırmaktır. Paralellik (parallelism) ise birden fazla işi gerçekten aynı anda yapmakla ilgilidir — birden fazla CPU çekirdeğinde işlemleri gerçekten eşzamanlı olarak yürütmektir.
| Kavram | Tanım | Örnek |
|---|---|---|
| Eşzamanlılık (Concurrency) | Birden fazla görev iç içe geçmiş şekilde ilerler | Tek çekirdeğin görevler arasında geçiş yapması |
| Paralellik (Parallelism) | Birden fazla görev gerçekten aynı anda çalışır | Birden fazla çekirdeğin aynı anda çalışması |
| Sıralı (Sequential) | Sırayla, tek seferde bir görev | Hiçbir örtüşme yok |
Yararlı bir zihinsel model: eşzamanlılık bir yapıdır (işi nasıl organize ettiğinizdir), paralellik ise bir yürütme özelliğidir (donanımın işi gerçekte nasıl çalıştırdığıdır). Paralellik olmadan eşzamanlılık olabilir (örneğin tek çekirdekte çalışan asyncio), ve “çok sayıda küçük görev” anlamında fazla eşzamanlılık olmadan da paralellik olabilir (örneğin büyük bir matris çarpımının çekirdeklere bölünmesi).
Python üç ana eşzamanlılık modeli sunar:
- Threading (İş Parçacığı) — I/O ağırlıklı işler için iyidir, CPU ağırlıklı işlerde GIL nedeniyle sınırlıdır.
- Multiprocessing (Çoklu İşlem) — CPU ağırlıklı işler için iyidir, GIL’i aşmak için ayrı işlemler (process) kullanır.
- Asyncio — çok yüksek eşzamanlılık gerektiren (binlerce bağlantı) I/O ağırlıklı işler için iyidir; tek iş parçacıklı, işbirlikçi (cooperative) çoklu görev yapısıdır.
2. Global Interpreter Lock (GIL)
GIL, Python nesnelerine erişimi koruyan bir mutex’tir; aynı süreç (process) içinde birden fazla native thread’in aynı anda Python bytecode’u çalıştırmasını engeller. Bunun nedeni, CPython’un bellek yönetiminin (referans sayımı) varsayılan olarak thread-safe olmamasıdır.
GIL neden var?
- Çöp toplama (garbage collection) için referans sayımının atomik olması gerekir; GIL olmadan, yarış koşulları (race condition) belleği bozabilir.
- CPython’un uygulanmasını basitleştirir ve tek iş parçacıklı kodu hızlı hale getirir.
- Çoğu C uzantısının thread-safety konusunda endişelenmesi gerekmediğinden, C uzantılarının yazılmasını kolaylaştırır.
Pratik etkileri
- Standart CPython’da CPU ağırlıklı çok iş parçacıklı kod, daha fazla thread ile HIZLANMAZ, çünkü aynı anda yalnızca bir thread Python bytecode’u çalıştırabilir.
- I/O ağırlıklı kod, thread’lerden fayda sağlar çünkü bloklayan I/O işlemleri sırasında (dosya okuma, ağ çağrıları,
time.sleepvb.) GIL serbest bırakılır. - GIL, CPU ağırlıklı işler sırasında bile diğer thread’lerin çalışmasına izin vermek için periyodik olarak serbest bırakılır (varsayılan olarak
sys.setswitchinterval()ile yaklaşık 5ms’de bir, yapılandırılabilir); bu, bir miktar iç içe geçmeye izin verir ancak gerçek paralel hesaplama sağlamaz.
import sys
print(sys.getswitchinterval()) # Varsayılan: 0.005 saniye
sys.setswitchinterval(0.001) # Daha sık geçiş yap
Gelecek: Free-Threaded Python (PEP 703)
Python 3.13 itibarıyla, GIL’i tamamen kaldıran deneysel bir free-threaded (serbest thread’li) build (python3.13t) mevcuttur. Bu, (başlangıçta Sam Gross tarafından “nogil” olarak önerilen) büyük ve devam eden bir çalışmadır. Bazı ödünleşimleri vardır:
- Nesne başına kilitleme (locking) ek yükü ve yanlı referans sayımı (biased reference counting) nedeniyle tek iş parçacıklı performans biraz daha yavaş olabilir.
- Birçok C uzantısı henüz uyumlu değildir.
- Henüz varsayılan build değildir; özellikle derlenmesi veya kurulması gerekir.
Free-threaded Python yaygınlaşıp ekosistem tarafından evrensel olarak desteklenene kadar, aşağıdaki pratik öneri (I/O için thread, CPU için process) standart yaklaşım olmaya devam etmektedir.
GIL’in etkisini gösteren örnek
import threading
import time
def cpu_bound_task(n):
count = 0
for i in range(n):
count += i * i
return count
def run_single_threaded():
start = time.perf_counter()
cpu_bound_task(50_000_000)
cpu_bound_task(50_000_000)
print(f"Tek thread: {time.perf_counter() - start:.2f}s")
def run_multi_threaded():
start = time.perf_counter()
t1 = threading.Thread(target=cpu_bound_task, args=(50_000_000,))
t2 = threading.Thread(target=cpu_bound_task, args=(50_000_000,))
t1.start(); t2.start()
t1.join(); t2.join()
print(f"Çoklu thread: {time.perf_counter() - start:.2f}s")
run_single_threaded()
run_multi_threaded()
# Standart CPython'da, CPU ağırlıklı işler için çoklu thread anlamlı bir şekilde daha hızlı DEĞİLDİR
3. threading Modülü
Thread’ler hafiftir ve bir süreç (process) içinde aynı bellek alanını paylaşırlar. I/O ağırlıklı işler için en uygun olanlardır: ağ istekleri, dosya I/O, veritabanı sorguları, harici kaynakları bekleme.
Temel thread oluşturma
import threading
import time
def worker(name, delay):
print(f"Thread {name} başlıyor")
time.sleep(delay)
print(f"Thread {name} bitti")
threads = []
for i in range(5):
t = threading.Thread(target=worker, args=(f"Worker-{i}", 1))
threads.append(t)
t.start()
for t in threads:
t.join() # Tüm thread'lerin tamamlanmasını bekle
print("Tüm thread'ler bitti")
Thread sınıfını miras almak (subclassing)
class MyThread(threading.Thread):
def __init__(self, name):
super().__init__()
self.name = name
def run(self):
print(f"{self.name} çalışıyor")
t = MyThread("özel-thread")
t.start()
t.join()
Daemon thread’ler
Daemon thread’ler, normal thread’lerin aksine ana program çıkarken otomatik olarak sonlandırılır. Normal thread’ler ise tamamlanana kadar programın çıkışını engeller.
t = threading.Thread(target=worker, args=("daemon", 10), daemon=True)
t.start()
# Bu thread hâlâ çalışıyor olsa bile program çıkabilir
Thread-local veriler
threading.local(), her thread’e kendi izole depolama alanını verir — veritabanı bağlantıları gibi thread başına durum (state) için kullanışlıdır.
local_data = threading.local()
def process():
local_data.value = threading.current_thread().name
print(local_data.value)
for i in range(3):
threading.Thread(target=process).start()
ThreadPoolExecutor (önizleme — Bölüm 6’da detaylı işlenmiştir)
from concurrent.futures import ThreadPoolExecutor
def fetch(url):
# ağ çağrısını simüle et
time.sleep(0.5)
return f"{url} adresinden veri"
urls = ["http://a.com", "http://b.com", "http://c.com"]
with ThreadPoolExecutor(max_workers=3) as executor:
results = list(executor.map(fetch, urls))
print(results)
Yarış koşulları (race condition) ve kilitlerin önemi
counter = 0
lock = threading.Lock()
def increment_unsafe():
global counter
for _ in range(100_000):
counter += 1 # Atomik DEĞİL! oku-değiştir-yaz yarış koşulu
def increment_safe():
global counter
for _ in range(100_000):
with lock:
counter += 1 # Kilit sayesinde atomik
threads = [threading.Thread(target=increment_unsafe) for _ in range(4)]
for t in threads: t.start()
for t in threads: t.join()
print(counter) # Yarış koşulları nedeniyle muhtemelen 400.000 DEĞİL
4. multiprocessing Modülü
Süreçlerin (process) ayrı bellek alanları vardır, bu da GIL’i tamamen atlamalarını sağlar. CPU ağırlıklı işler için idealdir: sayısal hesaplama, görüntü/video işleme, veri dönüşümü, simülasyonlar.
Temel süreç oluşturma
import multiprocessing as mp
def cpu_task(n):
return sum(i * i for i in range(n))
if __name__ == "__main__": # Windows/macOS'te (spawn yöntemi) ZORUNLU
p = mp.Process(target=cpu_task, args=(10_000_000,))
p.start()
p.join()
Paralel eşleme (mapping) için Pool
import multiprocessing as mp
def square(x):
return x * x
if __name__ == "__main__":
with mp.Pool(processes=4) as pool:
results = pool.map(square, range(20))
print(results)
# Diğer Pool metotları:
result_async = pool.apply_async(square, (10,))
print(result_async.get())
# imap: tembel (lazy), sırayı korur
for r in pool.imap(square, range(5)):
print(r)
# imap_unordered: tembel, sıra garanti değil, genellikle daha hızlı
for r in pool.imap_unordered(square, range(5)):
print(r)
Süreçler arasında veri paylaşımı
Süreçler belleği paylaşmadığından, açık mekanizmalara ihtiyacınız vardır:
from multiprocessing import Process, Value, Array, Manager
def increment(shared_val, shared_arr):
with shared_val.get_lock():
shared_val.value += 1
shared_arr[0] += 10
if __name__ == "__main__":
val = Value('i', 0) # paylaşılan tamsayı
arr = Array('i', [0, 0, 0]) # paylaşılan dizi
processes = [Process(target=increment, args=(val, arr)) for _ in range(4)]
for p in processes: p.start()
for p in processes: p.join()
print(val.value, arr[:])
# Daha karmaşık paylaşılan nesneler için Manager (dict, list)
with Manager() as manager:
shared_dict = manager.dict()
shared_list = manager.list()
shared_dict["key"] = "value"
shared_list.append(1)
Süreçler arası iletişim: Pipe’lar ve Kuyruklar
from multiprocessing import Process, Pipe, Queue
def pipe_worker(conn):
conn.send("alt süreçten merhaba")
conn.close()
def queue_worker(q):
q.put("kuyruk üzerinden alt süreçten merhaba")
if __name__ == "__main__":
# Pipe: iki yönlü, iki süreç için
parent_conn, child_conn = Pipe()
p = Process(target=pipe_worker, args=(child_conn,))
p.start()
print(parent_conn.recv())
p.join()
# Queue: çoktan çoğa, süreç-güvenli (process-safe)
q = Queue()
p2 = Process(target=queue_worker, args=(q,))
p2.start()
print(q.get())
p2.join()
Süreç başlatma yöntemleri (start methods)
| Yöntem | Açıklama | Varsayılan İşletim Sistemi |
|---|---|---|
fork | Alt süreç, ana sürecin belleğini kopyalar (hızlı, ancak thread’lerle güvensiz olabilir) | Linux, macOS (3.8 öncesi) |
spawn | Yeni bir Python yorumlayıcısı (interpreter) başlatılır, modülü temiz bir şekilde import eder | Windows, macOS (3.8+) |
forkserver | Bir sunucu süreci yeni worker’lar fork eder, bazı fork sorunlarından kaçınır | Unix’te mevcut |
import multiprocessing as mp
mp.set_start_method('spawn') # süreçler oluşturulmadan önce bir kez çağrılmalı
Multiprocessing’in maliyetleri
- Serileştirme (serialization) ek yükü: Süreçler arasında geçirilen veriler pickle’lanmalı/unpickle’lanmalıdır.
- Bellek ek yükü: Her sürecin kendi Python yorumlayıcısı ve bellek kopyası vardır.
- Başlatma gecikmesi: Bir süreç oluşturmak, bir thread oluşturmaktan daha yavaştır.
- Çok sayıda küçük görev yerine, iri taneli (coarse-grained) paralellik (büyük iş parçaları) için en uygunudur.
5. asyncio Modülü
asyncio, bir olay döngüsü (event loop) kullanarak tek iş parçacıklı, tek süreçli işbirlikçi çoklu görev (cooperative multitasking) sağlar. Thread’lere göre çok daha düşük ek yükle, çok yüksek eşzamanlılık gerektiren I/O ağırlıklı iş yüklerinde (örneğin binlerce eşzamanlı ağ bağlantısı) üstündür.
Temel kavramlar
- Coroutine: duraklatılıp devam ettirilebilen,
async defile tanımlanan bir fonksiyon. - Event loop (olay döngüsü): coroutine’leri, callback’leri ve I/O’yu zamanlayıp çalıştıran motor.
- Task: bir coroutine’i sarmalayarak event loop üzerinde çalışmasını zamanlayan yapı.
- Future: eninde sonunda bir sonucu temsil eden düşük seviyeli awaitable (beklenebilir) nesne.
- await: beklenen işlem tamamlanana kadar coroutine’i duraklatır, kontrolü tekrar event loop’a devreder.
Temel örnek
import asyncio
async def say_after(delay, message):
await asyncio.sleep(delay)
print(message)
async def main():
print("başladı")
await say_after(1, "merhaba")
await say_after(1, "dünya")
print("bitti") # Toplam: ~2 saniye (sıralı await'ler)
asyncio.run(main())
Görevleri eşzamanlı çalıştırma
import asyncio
async def say_after(delay, message):
await asyncio.sleep(delay)
print(message)
return message
async def main():
# Sıralı: toplam ~2 saniye
# await say_after(1, "merhaba")
# await say_after(1, "dünya")
# Eşzamanlı: toplam ~1 saniye
task1 = asyncio.create_task(say_after(1, "merhaba"))
task2 = asyncio.create_task(say_after(1, "dünya"))
await task1
await task2
asyncio.run(main())
Birden çok coroutine çalıştırmak için asyncio.gather
import asyncio
async def fetch_data(id):
await asyncio.sleep(1)
return f"veri-{id}"
async def main():
results = await asyncio.gather(
fetch_data(1),
fetch_data(2),
fetch_data(3),
)
print(results) # ['veri-1', 'veri-2', 'veri-3'] — hepsi ~1 saniyede
asyncio.run(main())
asyncio.TaskGroup (Python 3.11+) — yapılandırılmış eşzamanlılık
import asyncio
async def fetch_data(id):
await asyncio.sleep(1)
if id == 2:
raise ValueError("id 2'de hata oluştu")
return f"veri-{id}"
async def main():
try:
async with asyncio.TaskGroup() as tg:
for i in range(3):
tg.create_task(fetch_data(i))
except* ValueError as eg:
print(f"Yakalanan istisna grubu: {eg.exceptions}")
asyncio.run(main())
TaskGroup, bir görev başarısız olursa kardeş görevleri otomatik olarak iptal eder ve istisnaları doğru şekilde yayar — manuel olarak bir görev listesi yönetmekten çok daha güvenlidir.
Zaman aşımı (timeout) ve iptal
import asyncio
async def slow_operation():
await asyncio.sleep(5)
return "tamamlandı"
async def main():
try:
result = await asyncio.wait_for(slow_operation(), timeout=2)
except asyncio.TimeoutError:
print("İşlem zaman aşımına uğradı")
# Python 3.11+ timeout context manager
try:
async with asyncio.timeout(2):
await slow_operation()
except TimeoutError:
print("asyncio.timeout ile zaman aşımına uğradı")
asyncio.run(main())
Asenkron context manager’lar ve iterator’lar
class AsyncResource:
async def __aenter__(self):
print("kaynak alınıyor")
await asyncio.sleep(0.1)
return self
async def __aexit__(self, exc_type, exc, tb):
print("kaynak serbest bırakılıyor")
await asyncio.sleep(0.1)
async def main():
async with AsyncResource() as res:
print("kaynak kullanılıyor")
class AsyncCounter:
def __init__(self, limit):
self.limit = limit
self.i = 0
def __aiter__(self):
return self
async def __anext__(self):
if self.i >= self.limit:
raise StopAsyncIteration
await asyncio.sleep(0.1)
self.i += 1
return self.i
async def iterate():
async for num in AsyncCounter(3):
print(num)
asyncio.run(main())
asyncio.run(iterate())
asyncio içinde bloklayan / CPU ağırlıklı kod çalıştırma
Bloklayan çağrılar (time.sleep veya CPU ağırlıklı döngüler gibi) tüm event loop’u dondurur ve diğer tüm coroutine’leri engeller. Bu işi devretmek için executor’ları kullanın.
import asyncio
import time
def blocking_io():
time.sleep(2) # bloklayan I/O simülasyonu — bu, event loop'u dondururdu
return "bloklayan sonuç"
async def main():
loop = asyncio.get_running_loop()
# Bir thread pool'a devret
result = await loop.run_in_executor(None, blocking_io)
print(result)
# Ya da asyncio.to_thread kullan (Python 3.9+, daha basit API)
result2 = await asyncio.to_thread(blocking_io)
print(result2)
asyncio.run(main())
Asenkron generator’lar ve akış (streaming)
async def async_range(n):
for i in range(n):
await asyncio.sleep(0.1)
yield i
async def main():
async for i in async_range(5):
print(i)
asyncio.run(main())
Üretici-tüketici (producer-consumer) desenleri için asyncio.Queue
import asyncio
import random
async def producer(queue, n):
for i in range(n):
item = f"öge-{i}"
await queue.put(item)
print(f"Üretildi: {item}")
await asyncio.sleep(random.uniform(0.1, 0.3))
await queue.put(None) # tamamlandığını belirten sinyal (sentinel)
async def consumer(queue):
while True:
item = await queue.get()
if item is None:
break
print(f"Tüketildi: {item}")
queue.task_done()
async def main():
queue = asyncio.Queue()
await asyncio.gather(producer(queue, 5), consumer(queue))
asyncio.run(main())
6. concurrent.futures Modülü
Hem thread’ler hem de süreçler için yüksek seviyeli, birleşik bir arayüz sağlar ve çoğu manuel muhasebeyi (bookkeeping) soyutlar.
ThreadPoolExecutor
from concurrent.futures import ThreadPoolExecutor, as_completed
import time
def fetch(url):
time.sleep(1)
return f"{url} adresinden sonuç"
urls = [f"http://site{i}.com" for i in range(5)]
with ThreadPoolExecutor(max_workers=3) as executor:
# Yöntem 1: map (sırayı korur, hepsi bitene kadar bloklar)
for result in executor.map(fetch, urls):
print(result)
# Yöntem 2: submit + as_completed (sonuçları bitiş sırasına göre işle)
futures = {executor.submit(fetch, url): url for url in urls}
for future in as_completed(futures):
url = futures[future]
try:
data = future.result()
print(f"{url}: {data}")
except Exception as exc:
print(f"{url} bir istisna üretti: {exc}")
ProcessPoolExecutor
from concurrent.futures import ProcessPoolExecutor
import math
def is_prime(n):
if n < 2:
return False
for i in range(2, int(math.sqrt(n)) + 1):
if n % i == 0:
return False
return True
if __name__ == "__main__":
numbers = list(range(100_000, 100_100))
with ProcessPoolExecutor(max_workers=4) as executor:
results = list(executor.map(is_prime, numbers))
primes = [n for n, p in zip(numbers, results) if p]
print(primes)
Future’larda istisna yönetimi
from concurrent.futures import ThreadPoolExecutor
def risky(x):
if x == 3:
raise ValueError("kötü değer")
return x * 2
with ThreadPoolExecutor() as executor:
futures = [executor.submit(risky, i) for i in range(5)]
for f in futures:
try:
print(f.result())
except ValueError as e:
print(f"Yakalandı: {e}")
add_done_callback ile geri çağrılar (callback)
from concurrent.futures import ThreadPoolExecutor
import time
def task(n):
time.sleep(1)
return n * n
def callback(future):
print(f"Görev şu sonuçla tamamlandı: {future.result()}")
with ThreadPoolExecutor() as executor:
future = executor.submit(task, 5)
future.add_done_callback(callback)
7. Senkronizasyon Araçları
Birden fazla thread (veya süreç) paylaşılan durum (shared state) üzerinde işlem yaptığında, yarış koşullarını, deadlock’ları ve veri bozulmasını önlemek için senkronizasyona ihtiyaç vardır.
Lock (mutex)
Bir kritik bölüme aynı anda yalnızca bir thread’in erişmesini sağlar.
import threading
lock = threading.Lock()
shared_resource = []
def add_item(item):
with lock: # lock.acquire() / lock.release() ile eşdeğerdir
shared_resource.append(item)
RLock (yeniden girilebilir kilit)
Aynı thread’in kendini kilitlemeden (deadlock yapmadan) kilidi birden çok kez almasına izin verir — recursive (özyinelemeli) fonksiyonlar için kullanışlıdır.
import threading
rlock = threading.RLock()
def recursive_function(n):
with rlock:
if n > 0:
print(n)
recursive_function(n - 1) # Aynı thread kilidi tekrar alır, deadlock yok
Semaphore
Bir kaynağa aynı anda erişebilecek thread sayısını sınırlar — bağlantı havuzları (connection pool), hız sınırlama (rate-limiting) için kullanışlıdır.
import threading
import time
semaphore = threading.Semaphore(3) # aynı anda yalnızca 3 thread
def limited_resource(name):
with semaphore:
print(f"{name} kaynağı aldı")
time.sleep(1)
print(f"{name} kaynağı serbest bıraktı")
for i in range(10):
threading.Thread(target=limited_resource, args=(f"Thread-{i}",)).start()
BoundedSemaphore
Semaphore gibidir ancak alındığından daha fazla serbest bırakılırsa hata verir — programlama hatalarını yakalar.
Event
Thread’lerin bekleyebileceği basit bir bayrak (flag), bir-çoka sinyal göndermek için kullanılır.
import threading
import time
event = threading.Event()
def waiter():
print("Olay bekleniyor...")
event.wait() # event.set() çağrılana kadar bloklar
print("Olay alındı, devam ediliyor!")
def setter():
time.sleep(2)
print("Olay ayarlanıyor")
event.set()
threading.Thread(target=waiter).start()
threading.Thread(target=setter).start()
Condition
Bir kilidi bir bildirim (notification) bekleme yeteneğiyle birleştirir — üretici-tüketici (producer-consumer) desenlerinde kullanılır.
import threading
condition = threading.Condition()
items = []
def consumer():
with condition:
while not items:
condition.wait() # kilidi serbest bırakır, notify'ı bekler
item = items.pop(0)
print(f"Tüketildi: {item}")
def producer():
with condition:
items.append("öge")
print("Öge üretildi")
condition.notify() # bekleyen bir thread'i uyandırır
threading.Thread(target=consumer).start()
threading.Thread(target=producer).start()
Barrier
Bir grup thread’in hepsi bariyer noktasına ulaşana kadar beklemesini sağlar.
import threading
barrier = threading.Barrier(3)
def worker(name):
print(f"{name} bariyerde bekliyor")
barrier.wait() # 3 thread'in hepsi wait() çağırana kadar bloklar
print(f"{name} bariyeri geçti")
for i in range(3):
threading.Thread(target=worker, args=(f"Worker-{i}",)).start()
Deadlock’lar: nasıl oluşurlar ve nasıl önlenirler
Bir deadlock, iki veya daha fazla thread’in birbirini süresiz olarak beklemesi durumunda oluşur.
import threading
lock_a = threading.Lock()
lock_b = threading.Lock()
def thread_1():
with lock_a:
# ... bir iş yapar
with lock_b: # thread_2 zaten lock_b'yi tutuyor olabilir
pass
def thread_2():
with lock_b:
with lock_a: # thread_1 zaten lock_a'yı tutuyor olabilir — DEADLOCK
pass
Önleme stratejileri:
- Tüm thread’ler genelinde kilitleri her zaman aynı global sırayla alın.
- Süresiz beklemeyi önlemek için
lock.acquire(timeout=...)kullanın. - Kilitli bölümlerin kapsamını/süresini minimumda tutun.
- Mümkün olduğunda manuel kilitleme yerine daha üst seviye yapıları (
Queue,concurrent.futures) tercih edin.
asyncio senkronizasyon araçları
asyncio, coroutine’ler için tasarlanmış benzer araçlar sağlar: asyncio.Lock, asyncio.Event, asyncio.Condition, asyncio.Semaphore, asyncio.Barrier (3.11+). Bunlar thread-safe DEĞİLDİR ve yalnızca tek bir event loop içinde kullanılmalıdır.
import asyncio
async def main():
lock = asyncio.Lock()
async with lock:
print("kritik bölüm")
asyncio.run(main())
8. Kuyruklar ve Görevler Arası İletişim
Kuyruklar, eşzamanlı çalışma birimleri arasında veri geçirmek için thread/süreç-güvenli veri yapılarıdır — genellikle kilitlere güvenmek yerine veri paylaşmanın en güvenli yoludur.
queue.Queue (threading için)
import queue
import threading
q = queue.Queue(maxsize=10)
def producer():
for i in range(5):
q.put(f"öge-{i}")
def consumer():
while True:
item = q.get()
if item is None:
break
print(f"Alındı: {item}")
q.task_done()
threading.Thread(target=producer).start()
t = threading.Thread(target=consumer)
t.start()
q.join() # tüm ögeler işlenene kadar bekle
q.put(None)
t.join()
Kuyruk çeşitleri: Queue (FIFO), LifoQueue (yığın/stack gibi), PriorityQueue (önceliğe göre sıralı).
multiprocessing.Queue
Aynı API, ancak süreçler arasında paylaşmak güvenlidir (dahili olarak pipe’lar ve kilitler kullanır, veriyi pickle’lar).
asyncio.Queue
Kavramsal olarak aynı API, ancak coroutine’ler için tasarlanmıştır (await queue.put(), await queue.get()).
9. Doğru Aracı Seçmek
| İş Yükü Türü | Önerilen Araç | Neden |
|---|---|---|
| I/O ağırlıklı, az bağlantı | threading | Basit, iyi çalışır, I/O sırasında GIL serbest bırakılır |
| I/O ağırlıklı, binlerce bağlantı | asyncio | Thread’lere göre görev başına son derece düşük ek yük |
| CPU ağırlıklı (ağır hesaplama) | multiprocessing / ProcessPoolExecutor | GIL’i aşar, birden fazla çekirdek kullanır |
| Karma I/O + hafif CPU işi | asyncio + run_in_executor/to_thread | Event loop’u tepkisel (responsive) tutar |
| Veri üzerinde basit paralel “map” | concurrent.futures (Thread veya Process pool) | Yüksek seviyeli, daha az standart kod (boilerplate) |
| Worker’lar arasında paylaşılan değiştirilebilir durum gerekli | threading (kilitlerle) veya multiprocessing.Manager | Süreçlerin açık paylaşım mekanizmalarına ihtiyacı vardır |
Karar akış şeması (metin olarak)
- Görev I/O ağırlıklı mı (ağ, disk, harici sistemleri bekleme)?
- Evet ve tüm çağrı yığınını (call stack) async kütüphaneleri kullanılabilir şekilde kontrol ediyorsanız →
asynciokullanın. - Evet, ancak bloklayan kütüphanelerle çalışıyorsanız (örneğin eski veritabanı sürücüleri) →
threadingveyaThreadPoolExecutorkullanın.
- Evet ve tüm çağrı yığınını (call stack) async kütüphaneleri kullanılabilir şekilde kontrol ediyorsanız →
- Görev CPU ağırlıklı mı (hesaplama yoğun, sıkı döngüler, sayı işleme)?
- Evet →
multiprocessingveyaProcessPoolExecutorkullanın. - NumPy/Cython/native kütüphanelerin GIL’i serbest bırakıp bırakmadığını düşünün — eğer öyleyse, threading yine de yardımcı olabilir.
- Evet →
- Aşırı yüksek eşzamanlılığa (10.000+ eşzamanlı bağlantı) ihtiyacınız var mı?
- Evet →
asyncioesasen zorunludur; thread’ler çok pahalı hale gelir (bellek/context switching).
- Evet →
10. Yaygın Hatalar ve İyi Uygulamalar
Hata 1: Thread’lerin CPU ağırlıklı Python kodunu hızlandırdığına inanmak
Daha önce tartışıldığı gibi, GIL standart CPython’da bunu engeller. Bunun yerine multiprocessing kullanın.
Hata 2: multiprocessing ile if __name__ == "__main__": korumasını unutmak
Windows ve macOS’ta (spawn yöntemi), bu korumayı unutmak, alt sürecin ana modülü yeniden import etmesi nedeniyle sonsuz süreç oluşturmaya veya çökmelere neden olur.
# Windows/macOS'te YANLIŞ
import multiprocessing as mp
def worker():
print("çalışıyor")
p = mp.Process(target=worker)
p.start() # Bu satır oluşturulan her alt süreç tarafından tekrar çalıştırılır = kaos
# DOĞRU
if __name__ == "__main__":
p = mp.Process(target=worker)
p.start()
p.join()
Hata 3: asyncio event loop’unu bloklamak
Bir coroutine içinde doğrudan time.sleep() çağırmak veya ağır CPU işi yapmak, tüm event loop’u dondurur.
# YANLIŞ
async def bad():
time.sleep(5) # HER ŞEYİ bloklar
# DOĞRU
async def good():
await asyncio.sleep(5) # Bloklamaz
# ya da CPU ağırlıklı işi devret:
await asyncio.to_thread(cpu_heavy_function)
Hata 4: Korunmayan paylaşılan durumdan kaynaklanan yarış koşulları
# YANLIŞ - yarış koşulu
counter = 0
def increment():
global counter
counter += 1 # Atomik değil
# DOĞRU
lock = threading.Lock()
def increment_safe():
global counter
with lock:
counter += 1
Hata 5: Tutarsız kilit sıralamasından kaynaklanan deadlock’lar
Kod tabanı genelinde birden fazla kilidi her zaman tutarlı bir global sırayla alın.
Hata 6: Görevleri join/await etmeyi unutmak (sahipsiz kalan işler)
# Thread'ler: t.join() çağırmayı unutmak, program iş bitmeden çıkmasına
# ya da daemon yapılmadıysa zombi thread'ler kalmasına neden olabilir.
# Asyncio: bir görevi await etmeyi unutmak "fırlat ve unut" (fire and forget)
# anlamına gelir — içindeki istisnalar, bir referans tutup kontrol etmediğiniz
# ya da bunu doğru şekilde ele alan TaskGroup kullanmadığınız sürece
# sessizce yutulur.
Hata 7: Süreçler arasında mutable varsayılan argümanlar / paylaşılan nesneler kullanmak
Büyük, karmaşık veya pickle’lanamayan nesneleri multiprocessing.Process/Pool‘a geçirmek hatalara veya serileştirme ek yüküne neden olur. Worker fonksiyon argümanlarını küçük ve basit tutun (sayılar, string’ler, primitif listeler).
Hata 8: Ağır üretici yükü altında kuyruklar için maksimum boyut belirlememek
maxsize olmadan, tüketici yetişemiyorsa hızlı bir üretici sınırsız bellek büyümesine neden olabilir.
İyi uygulamalar özeti
- Manuel thread/süreç yönetimi yerine yüksek seviyeli API’leri (
concurrent.futures,asyncio.TaskGroup) tercih edin. - Temizliği garanti etmek için context manager’ları (
with lock:,with ThreadPoolExecutor() as ex:) kullanın. - Kritik bölümleri (kilitli kodu) olabildiğince kısa tutun.
- Mümkün olduğunda paylaşılan mutable durum yerine mesaj geçişini (kuyruklar) tercih edin.
- Future/görevlerden gelen istisnaları her zaman ele alın — bunlar programı otomatik olarak çökertmez ama sessizce kaybolabilirler.
- Paralelleştirmeden önce profilleyin — eşzamanlılık karmaşıklık ekler ve bazen ek yük, küçük görevler için faydadan ağır basar.
- Hedef işletim sisteminde test edin —
forkvespawnfarkları yalnızca belirli platformlarda ortaya çıkan hatalara neden olabilir. - Süresiz asılı kalmayı önlemek için kilitlerde, join’lerde ve ağ çağrılarında zaman aşımları kullanın.
11. İleri Düzey Konular
asyncio ve thread’leri birlikte kullanmak: event loop’a güvenli şekilde geri çağrı yapmak
Bir arka plan thread’inin asyncio ile etkileşime girmesi gerektiğinde, run_coroutine_threadsafe kullanın:
import asyncio
import threading
async def notify(message):
print(f"Bildirildi: {message}")
def background_worker(loop):
import time
time.sleep(1)
asyncio.run_coroutine_threadsafe(notify("thread'den tamamlandı"), loop)
async def main():
loop = asyncio.get_running_loop()
threading.Thread(target=background_worker, args=(loop,)).start()
await asyncio.sleep(2)
asyncio.run(main())
Yapılandırılmış eşzamanlılık (structured concurrency) felsefesi
Modern eşzamanlı programlama, yapılandırılmış eşzamanlılığı tercih eder: eşzamanlı görevlerin, sızabilen ya da amaçlanan bağlamından daha uzun yaşayabilen “fırlat ve unut” görevleri yerine, bir kapsama (scope) bağlı net ve sınırlı yaşam sürelerine sahip olması gerekir (TaskGroup veya async with gibi). Bu, hata yönetimini ve iptali öngörülebilir hale getirir.
Multiprocessing’i asyncio ile birleştirmek
Async bir uygulama içindeki CPU ağırlıklı işler için, bir ProcessPoolExecutor‘a devredin:
import asyncio
from concurrent.futures import ProcessPoolExecutor
def cpu_heavy(n):
return sum(i * i for i in range(n))
async def main():
loop = asyncio.get_running_loop()
with ProcessPoolExecutor() as pool:
result = await loop.run_in_executor(pool, cpu_heavy, 10_000_000)
print(result)
if __name__ == "__main__":
asyncio.run(main())
multiprocessing.shared_memory (Python 3.8+)
Süreçler arasında gerçek sıfır kopyalama (zero-copy) paylaşılan bellek bloklarına izin verir, NumPy dizileri gibi büyük veriler için pickle ek yükünden kaçınır.
from multiprocessing import shared_memory
import numpy as np
# Paylaşılan bellek bloğu oluştur
arr = np.array([1, 2, 3, 4, 5])
shm = shared_memory.SharedMemory(create=True, size=arr.nbytes)
shared_arr = np.ndarray(arr.shape, dtype=arr.dtype, buffer=shm.buf)
shared_arr[:] = arr[:]
# Başka bir süreçte, shm.name üzerinden aynı bloğa bağlan
existing_shm = shared_memory.SharedMemory(name=shm.name)
attached_arr = np.ndarray(arr.shape, dtype=arr.dtype, buffer=existing_shm.buf)
print(attached_arr)
shm.close()
shm.unlink() # paylaşılan belleği serbest bırak
Asenkron comprehension’lar
import asyncio
async def get_value(x):
await asyncio.sleep(0.1)
return x * 2
async def main():
results = [await get_value(x) for x in range(5)]
print(results)
# Asenkron generator comprehension
async def gen():
for i in range(5):
yield i
results2 = [x async for x in gen()]
print(results2)
asyncio.run(main())
uvloop — daha hızlı bir event loop
libuv üzerine inşa edilmiş, doğrudan değiştirilebilir (drop-in replacement) bir event loop; I/O ağırlıklı asyncio uygulamaları için önemli performans kazançları sağlar (standart kütüphanenin parçası değildir; pip ile kurulur).
import asyncio
import uvloop
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
C uzantılarında GIL serbest bırakma
NumPy gibi kütüphaneler, ağır sayısal işlemler sırasında GIL’i serbest bırakır; bu da, bu tür çağrıların hakim olduğu iş yükleri için threading’in (saf Python döngülerinin aksine) gerçek hız kazanımları sağlayabileceği anlamına gelir.
12. Özet Tablo
| Özellik | threading | multiprocessing | asyncio |
|---|---|---|---|
| Paralellik (çoklu çekirdek) | Hayır (GIL sınırlı) | Evet | Hayır (tek thread’li) |
| En iyi kullanım alanı | I/O ağırlıklı, az görev | CPU ağırlıklı | I/O ağırlıklı, çok görev |
| Bellek | Paylaşılan | Süreç başına ayrı | Paylaşılan (tek süreç) |
| Birim başına ek yük | Orta | Yüksek | Çok düşük |
| İletişim | Paylaşılan değişkenler + kilitler | Pipe’lar, Kuyruklar, paylaşılan bellek | Paylaşılan değişkenler (tek loop) + kuyruklar |
| Hata izolasyonu | Düşük (paylaşılan bellek) | Yüksek (ayrı süreç) | Düşük (tek süreç) |
| Hata ayıklama zorluğu | Orta-Yüksek | Orta | Orta (farklı zihinsel model) |
Açık await/callback gerektirir | Hayır | Hayır | Evet |
Hızlı referans import’ları
import threading # Thread, Lock, Event, Condition, Semaphore
import multiprocessing as mp # Process, Pool, Queue, Value, Array, Manager
import asyncio # coroutine'ler, task'lar, event loop
from concurrent.futures import (
ThreadPoolExecutor, ProcessPoolExecutor, as_completed
)
import queue # Queue, LifoQueue, PriorityQueue (thread-safe)
Altın kurallar
- I/O ağırlıklı + az görev → threading.
- I/O ağırlıklı + çok görev → asyncio.
- CPU ağırlıklı → multiprocessing.
- Manuel thread/süreç yönetimi yerine basit paralel map işlemleri için
concurrent.futures‘ı tercih edin. - Bloklayan kodu asla doğrudan bir coroutine içinde çağırmayın — devredin.
- Multiprocessing giriş noktalarını her zaman
if __name__ == "__main__":ile koruyun. - Paylaşılan mutable durum yerine mesaj geçişini (kuyruklar) tercih edin.
- Optimize etmeden önce ölçün — eşzamanlılık bedavaya gelmez.