Python uygulamalarınızın yavaş çalışmasından, kullanıcı arayüzünün donmasından veya aynı anda birden fazla görevi yönetmekte zorlanmaktan mı şikayetçisiniz? Engellemeyen kod yürütme, modern uygulama geliştirmede kritik bir öneme sahiptir ve Python’ın sunduğu Executor’lar, bu sorunu aşmanın en etkili yollarından biridir. Bu makale, Python’da eşzamanlı ve paralel programlamanın kalbi olan Executor mekanizmasını sıfırdan ele alacak, engellemeyen kodun ne olduğunu açıklayacak ve gerçek dünya senaryolarıyla uygulamalı örnekler sunarak uygulamalarınızın performansını nasıl artırabileceğinizi gösterecektir.
Günümüzün dijital dünyasında, kullanıcılar ve sistemler uygulamaların yalnızca işlevsel olmasını değil, aynı zamanda hızlı ve duyarlı olmasını da bekliyor. Bir web sunucusu düşünün ki her istekte veritabanına büyük bir sorgu gönderiyor; bu sorgu tamamlanana kadar diğer tüm istekler bekletilirse, sisteminiz anında tıkanır ve kullanıcı deneyimi ciddi şekilde olumsuz etkilenir. Veya yoğun bir veri işleme uygulamasında, tek bir büyük dosyanın işlenmesi diğer görevlerin saatlerce beklemesine neden olabilir. İşte bu senaryoların temelinde “engelleme” (blocking) adı verilen durum yatar.
Engellemeyen kod yürütme, bir görevin tamamlanmasını beklerken diğer görevlerin duraksamaması prensibine dayanır. Özellikle uzun süren I/O (Giriş/Çıkış) işlemleri (ağ çağrıları, dosya okuma/yazma, veritabanı işlemleri) veya yoğun CPU (İşlemci) hesaplamaları içeren Python uygulamalarında bu durum hayati önem taşır. Tek iş parçacıklı bir ortamda, bu tür engelleme yapan işlemler uygulamanın genel yanıt verme hızını düşürür ve performansı ciddi şekilde olumsuz etkiler. Kullanıcıların beklentileri arttıkça, arka planda karmaşık işlemler yürütülürken bile uygulamanın akıcı kalması zorunluluk haline gelmiştir. Python’ın yerleşik eşzamanlılık yetenekleri, özellikle concurrent.futures modülü altında sunulan Executor’lar, bu engelleme sorununu zarif ve verimli bir şekilde çözmek için güçlü araçlar sunar.
Executor’lar, belirli görevleri ayrı iş parçacıklarında (threads) veya ayrı süreçlerde (processes) çalıştırmayı soyutlayarak karmaşık eşzamanlı programlama detaylarını sizin yerinize yönetir. Bu sayede, uzun süren bir web isteğini beklerken, kullanıcının arayüzle etkileşime devam etmesini sağlayabilir; veya büyük bir veri setini işlerken, uygulamanın diğer bölümlerinin yanıt vermeye devam etmesini temin edebilirsiniz. Bu teknikler yalnızca uygulamanızın hızını artırmakla kalmaz, aynı zamanda daha sağlam, ölçeklenebilir ve kullanıcı dostu sistemler geliştirmenize olanak tanır. Python’ın basit ve okunabilir sözdizimi sayesinde, bu güçlü eşzamanlılık araçlarını öğrenmek ve uygulamak düşündüğünüzden çok daha kolaydır. Bu makale boyunca, bu araçları nasıl kullanacağınızı ve uygulamanızın “gizli yaşamını” nasıl daha verimli hale getireceğinizi adım adım keşfedeceğiz.
Temel Kavramlar: Engellemeyen Kod ve Python’ın Konkurent Dünya’sı
Python’da engellemeyen kod yazmaya başlamadan önce, bazı temel kavramları anlamak önemlidir. Bu kavramlar, Executor’ların nasıl çalıştığını ve hangi durumlarda hangi aracı kullanmanız gerektiğini belirlemenize yardımcı olacaktır.
-
Engelleme (Blocking) ve Engellememe (Non-blocking):
Engelleme, bir görevin (örneğin, bir ağ isteği veya dosya okuma) tamamlanmasını beklerken ana programın veya iş parçacığının duraklaması anlamına gelir. Bu süre zarfında CPU boştadır ve diğer görevler yürütülemez. Engellememe ise, bir görevin tamamlanmasını beklemek yerine, o görevi arka plana atarak ana programın diğer işlere devam etmesi durumudur. Görev tamamlandığında bir bildirim veya sonuç beklenir.
-
Eşzamanlılık (Concurrency) ve Paralelizm (Parallelism):
- Eşzamanlılık: Birden fazla görevin “aynı anda” ilerlemesi algısıdır, ancak tek bir CPU çekirdeğinde görevler arasında hızla geçiş yapılarak gerçekleşir. Görevler birbirini beklemek zorunda kalmadan ilerler. Bir garsonun aynı anda birden fazla masaya hizmet vermesi gibi düşünebilirsiniz; aslında her bir masaya kısa sürelerle odaklanıp tekrar diğerine geçer.
- Paralelizm: Birden fazla görevin gerçekten aynı anda (aynı anda farklı CPU çekirdeklerinde) yürütülmesidir. Bu, birden fazla garsonun aynı anda farklı masalara hizmet vermesine benzer. Paralelizm, performansı doğrudan artırır.
-
Python Global Interpreter Lock (GIL):
Python’ın eşzamanlılık dünyasında GIL, önemli bir kavramdır. GIL, bir anda yalnızca tek bir iş parçacığının (thread) Python bytecode’unu yürütmesine izin veren bir mekanizmadır. Bu, çok çekirdekli işlemcilerde bile Python’daki iş parçacıklarının (threading modülü) “gerçek” paralelizm sağlamasını engeller. Yani, CPU yoğun işlemlerde
ThreadPoolExecutorkullanmak, performansı çoğu zaman beklenen kadar artırmaz. Ancak GIL, I/O yoğun işlemleri etkilemez; çünkü bir iş parçacığı I/O beklerken GIL’i serbest bırakır ve diğer iş parçacıklarının çalışmasına izin verir. -
concurrent.futuresModülü:Python’da eşzamanlı görevleri yönetmenin modern ve tavsiye edilen yolu
concurrent.futuresmodülüdür. Bu modül, görevleri iş parçacıkları veya süreçler havuzuna göndermeyi soyutlar. Özellikle karmaşıkthreadingveyamultiprocessingAPI’lerini doğrudan kullanmak yerine, daha yüksek seviyeli ve kullanımı kolay bir arayüz sunar. İki ana sınıfı vardır:ThreadPoolExecutor(iş parçacıkları havuzu için) veProcessPoolExecutor(süreçler havuzu için).
Bu temel kavramları anladıktan sonra, Python uygulamalarınızda engellemeyen kod stratejilerini daha bilinçli bir şekilde uygulayabilirsiniz. Hangi Executor’ı seçeceğiniz, görevinizin I/O bağlı mı yoksa CPU bağlı mı olduğuna bağlı olacaktır. I/O bağlı görevler için ThreadPoolExecutor genellikle yeterliyken, CPU bağlı görevler için GIL’i aşmak adına ProcessPoolExecutor kullanmak çok daha mantıklıdır. Bu ayrım, uygulamanızın performansını optimize etmede kilit rol oynar.
ThreadPoolExecutor ile I/O Bağlı İşlemleri Hızlandırma: Bir Vaka Analizi
I/O (Giriş/Çıkış) bağlı işlemler, adından da anlaşılacağı gibi, işlemcinin hesaplama gücünden çok, dış kaynaklarla (ağ, disk, veritabanı vb.) iletişim kurma süresine bağlı olan görevlerdir. Bir web sitesinden veri çekmek, bir API’ye istek göndermek, diskten büyük bir dosyayı okumak veya yazmak, veritabanı sorguları çalıştırmak bu tür görevlere örnek verilebilir. Bu tür işlemler sırasında Python programınız, dış kaynaktan yanıt gelene kadar beklemek zorunda kalır ve bu bekleme süresi boyunca CPU genellikle boştadır. İşte tam da bu noktada ThreadPoolExecutor devreye girer ve uygulamanızın yanıt verme hızını inanılmaz derecede artırabilir.
ThreadPoolExecutor, belirli bir sayıda iş parçacığından (thread) oluşan bir havuz (pool) oluşturur. Siz ona görevler verdiğinizde, bu görevleri havuzdaki boşta olan bir iş parçacığına atar. Bir iş parçacığı I/O işlemi beklerken, Python’ın Global Interpreter Lock (GIL) serbest bırakılır ve diğer iş parçacıklarının (eğer varsa) Python kodunu yürütmesine izin verilir. Bu sayede, aynı anda birden fazla I/O işlemi başlatılabilir ve programınızın genel ilerlemesi duraksamaz. Bu, Python’ın GIL kısıtlamasının I/O bağlı görevler üzerindeki etkisini etkin bir şekilde aşmanın en yaygın ve en pratik yoludur.
Adım Adım ThreadPoolExecutor Kullanımı Nasıl Yapılır?
Bir senaryo düşünelim: Bir e-ticaret sitesi için birden fazla ürün sayfasının başlığını ve fiyatını çekmeniz gerekiyor. Her sayfa çekme işlemi bir ağ çağrısı gerektireceğinden bu bir I/O bağlı görevdir. Geleneksel (seri) yöntemle her sayfayı tek tek çekersek, toplam süre tüm sayfaların indirilme sürelerinin toplamı olacaktır. Ancak ThreadPoolExecutor ile bu indirme işlemlerini eşzamanlı hale getirebilir ve süreyi önemli ölçüde kısaltabiliriz.
import requests
from concurrent.futures import ThreadPoolExecutor, as_completed
import time
def fetch_page_title(url):
"""Belirtilen URL'den sayfa başlığını çeker."""
try:
response = requests.get(url, timeout=5) # 5 saniye zaman aşımı
response.raise_for_status() # HTTP hataları için hata fırlat
# Basit bir başlık çekme örneği, gerçekte HTML parse edilebilir
title_start = response.text.find("")
title_end = response.text.find(" ")
if title_start != -1 and title_end != -1:
return url, response.text[title_start+7:title_end].strip()
return url, "Başlık Bulunamadı"
except requests.exceptions.RequestException as e:
return url, f"Hata: {e}"
# Vaka Analizi: Farklı web sitelerinden başlıkları eşzamanlı çekme
urls = [
"https://www.python.org",
"https://docs.python.org/3/",
"https://www.google.com",
"https://www.bing.com",
"https://www.youtube.com",
"https://www.github.com",
"https://www.stackoverflow.com",
"https://www.wikipedia.org",
"https://www.amazon.com",
"https://www.microsoft.com",
"https://invalid-url-example.com" # Hata senaryosu için
]
print("Seri işlem başlıyor...")
start_time_serial = time.time()
serial_results = []
for url in urls:
serial_results.append(fetch_page_title(url))
end_time_serial = time.time()
print(f"Seri işlem tamamlandı. Süre: {end_time_serial - start_time_serial:.2f} saniye")
# for url, title in serial_results:
# print(f"Seri - URL: {url}, Başlık: {title}")
print("\nThreadPoolExecutor ile paralel işlem başlıyor...")
start_time_parallel = time.time()
# max_workers, aynı anda çalışacak maksimum iş parçacığı sayısıdır.
# Genellikle I/O bağlı işler için işlemci çekirdek sayısından çok daha fazla olabilir.
with ThreadPoolExecutor(max_workers=10) as executor:
# submit metodu, her bir URL için fetch_page_title fonksiyonunu çalıştırır
# ve bir Future objesi döndürür.
future_to_url = {executor.submit(fetch_page_title, url): url for url in urls}
# as_completed, Future objelerini tamamlanma sırasına göre döndürür
for future in as_completed(future_to_url):
url = future_to_url[future]
try:
# result() metodu, görevin sonucunu döndürür. Eğer görevde hata varsa,
# bu hatayı burada tekrar fırlatır.
url_result, title_result = future.result()
# print(f"Paralel - URL: {url_result}, Başlık: {title_result}")
except Exception as exc:
print(f"'{url}' için sayfa çekilirken hata oluştu: {exc}")
end_time_parallel = time.time()
print(f"Paralel işlem tamamlandı. Süre: {end_time_parallel - start_time_parallel:.2f} saniye")
Yukarıdaki örnekte, ThreadPoolExecutor kullanarak birden fazla web sayfasını eşzamanlı olarak çekme işlemini gerçekleştirdik. Önce serbest bir şekilde, her bir URL için sırayla fetch_page_title fonksiyonunu çağırarak bir karşılaştırma noktası oluşturduk. Ardından, ThreadPoolExecutor ile max_workers=10 ayarlayarak, aynı anda 10 farklı iş parçacığının web sayfalarını indirmesine izin verdik. executor.submit() her bir görevi havuza gönderir ve bir Future objesi döndürür. as_completed() ise bu Future objelerini tamamlandıkça sırayla verir, böylece sonuçları geldikçe işleyebiliriz.
max_workers değeri, ThreadPoolExecutor için genellikle 32 gibi yüksek bir sayıya kadar iyi performans verebilir. Özellikle çok sayıda küçük I/O işlemi yapıyorsanız, bu değeri yükseltmek performansı artırabilir. Ancak çok fazla iş parçacığı da sistem kaynaklarını tüketerek ters etki yaratabilir, bu nedenle deneme yanılma ile optimal değeri bulmak önemlidir.
Çıktıyı incelediğinizde, paralel işlemin seri işleme kıyasla çok daha kısa sürede tamamlandığını göreceksiniz. Bu, ThreadPoolExecutor'ın I/O bağlı görevlerde nasıl bir fark yaratabileceğinin açık bir göstergesidir. Hata yönetimi de önemlidir; future.result() metodunu bir try-except bloğu içine alarak, iş parçacıklarında meydana gelen hataları yakalayabilir ve uygulamanızın çökmesini önleyebilirsiniz. Bu mekanizma, sadece performans artışı sağlamakla kalmaz, aynı zamanda uygulamanızı daha dayanıklı hale getirir.
ProcessPoolExecutor ile CPU Bağlı İşlemleri Paralelleştirme: Gerçek Dünya Senaryosu
CPU (İşlemci) bağlı işlemler, adından da anlaşılacağı gibi, işlemcinin yoğun hesaplama gücüne ihtiyaç duyan görevlerdir. Büyük matris çarpımları, karmaşık algoritmik hesaplamalar, görüntü veya video işleme, bilimsel simülasyonlar, veri analizi ve makine öğrenimi modellerinin eğitimi bu tür görevlere örnek verilebilir. Python'ın Global Interpreter Lock (GIL) kısıtlaması nedeniyle, ThreadPoolExecutor gibi iş parçacığı tabanlı çözümler CPU bağlı görevlerde gerçek paralelizm sağlayamaz. Çünkü GIL, aynı anda yalnızca bir iş parçacığının Python bytecode'unu yürütmesine izin verir.
İşte bu noktada ProcessPoolExecutor devreye girer. ProcessPoolExecutor, görevleri ayrı işletim sistemi süreçlerine (processes) dağıtır. Her bir süreç kendi Python yorumlayıcısına ve kendi belleğine sahip olduğundan, GIL her süreçte ayrı ayrı etkin olur. Bu da farklı süreçlerin aynı anda farklı CPU çekirdeklerinde Python kodunu paralel olarak yürütmesine olanak tanır. Böylece, çok çekirdekli işlemcilerin gücünden tam anlamıyla faydalanarak CPU bağlı işlemlerin performansını çarpıcı bir şekilde artırabilirsiniz.
ProcessPoolExecutor ile Verimlilik Nasıl Artırılır?
Bir senaryo düşünelim: Büyük bir görüntü koleksiyonunu işlemek istiyorsunuz. Her bir görüntünün karmaşık bir filtreleme veya yeniden boyutlandırma işleminden geçmesi gerekiyor. Bu, her görüntü için yoğun CPU hesaplamaları gerektiren bir görevdir. Geleneksel olarak her görüntüyü tek tek işlemek uzun sürecektir. ProcessPoolExecutor kullanarak bu görüntü işleme görevlerini paralel hale getirebilir ve toplam süreyi önemli ölçüde azaltabilirsiniz.
from concurrent.futures import ProcessPoolExecutor, as_completed
import time
import os
from PIL import Image # Görüntü işleme için Pillow kütüphanesi
import numpy as np # Matris işlemleri için
# Örnek bir CPU yoğun görev: Görüntüye karmaşık bir filtre uygulama
def apply_complex_filter(image_path, output_dir="processed_images"):
"""
Belirtilen görüntüye CPU yoğun bir filtre uygular ve yeni bir dosya olarak kaydeder.
Bu, basit bir gri tonlama ve kenar bulma işlemi içerebilir.
"""
if not os.path.exists(output_dir):
os.makedirs(output_dir)
try:
img = Image.open(image_path).convert('L') # Gri tonlamaya dönüştür
img_array = np.array(img, dtype=np.float32)
# Basit bir kenar bulma filtresi (Laplacian benzeri)
kernel = np.array([
[-1, -1, -1],
[-1, 8, -1],
[-1, -1, -1]
], dtype=np.float32)
# Görüntüye filtreyi uygula (çok basit konvolüsyon örneği)
filtered_img_array = np.zeros_like(img_array)
rows, cols = img_array.shape
for r in range(1, rows - 1):
for c in range(1, cols - 1):
# Her pikselin etrafındaki komşularıyla matris çarpımı
region = img_array[r-1:r+2, c-1:c+2]
filtered_img_array[r, c] = np.sum(region * kernel)
# Sonucu 0-255 aralığına normalize et ve kaydet
filtered_img_array = np.clip(filtered_img_array, 0, 255)
processed_img = Image.fromarray(filtered_img_array.astype(np.uint8))
output_filename = os.path.join(output_dir, os.path.basename(image_path).replace('.', '_processed.'))
processed_img.save(output_filename)
return f"{image_path} başarıyla işlendi ve {output_filename} olarak kaydedildi."
except Exception as e:
return f"{image_path} işlenirken hata oluştu: {e}"
# Örnek görüntü dosyaları oluşturma (gerçek bir senaryoda bu dosyalar zaten var olacaktır)
def create_dummy_images(num_images=5):
if not os.path.exists("images"):
os.makedirs("images")
for i in range(num_images):
img = Image.new('RGB', (1000, 800), color = (i*50 % 255, (i*70+10) % 255, (i*90+20) % 255))
img.save(f"images/dummy_image_{i+1}.png")
return [f"images/dummy_image_{i+1}.png" for i in range(num_images)]
image_paths = create_dummy_images(10) # 10 adet örnek görüntü oluştur
print("Seri görüntü işleme başlıyor...")
start_time_serial = time.time()
serial_results = []
for img_path in image_paths:
serial_results.append(apply_complex_filter(img_path))
end_time_serial = time.time()
print(f"Seri işlem tamamlandı. Süre: {end_time_serial - start_time_serial:.2f} saniye")
# for res in serial_results:
# print(res)
print("\nProcessPoolExecutor ile paralel görüntü işleme başlıyor...")
start_time_parallel = time.time()
# max_workers, genellikle işlemci çekirdek sayısına eşit veya biraz fazla ayarlanır.
# os.cpu_count() ile CPU çekirdek sayısını otomatik alabiliriz.
with ProcessPoolExecutor(max_workers=os.cpu_count()) as executor:
future_to_image = {executor.submit(apply_complex_filter, img_path): img_path for img_path in image_paths}
for future in as_completed(future_to_image):
img_path = future_to_image[future]
try:
result = future.result()
# print(result)
except Exception as exc:
print(f"'{img_path}' işlenirken hata oluştu: {exc}")
end_time_parallel = time.time()
print(f"Paralel işlem tamamlandı. Süre: {end_time_parallel - start_time_parallel:.2f} saniye")
# Oluşturulan dummy imajları temizle (isteğe bağlı)
# for img_path in image_paths:
# os.remove(img_path)
# os.rmdir("images")
Bu örnekte, apply_complex_filter fonksiyonu görüntüye gri tonlama dönüştürme ve basit bir kenar bulma filtresi uyguluyor. Bu işlemler CPU üzerinde yoğun hesaplamalar gerektirir. create_dummy_images fonksiyonu, test için sahte görüntüler oluşturur. Kod, önce seri bir şekilde tüm görüntüleri işler, ardından ProcessPoolExecutor kullanarak aynı işlemi paralel olarak gerçekleştirir. max_workers parametresi genellikle sistemdeki CPU çekirdeği sayısına (os.cpu_count()) ayarlanır; bu, her bir çekirdeğin bir süreci çalıştırmasına izin vererek maksimum paralelizm sağlar.
Çıktıyı incelediğinizde, ProcessPoolExecutor ile yapılan paralel işlemin, seri işleme kıyasla ne kadar daha hızlı tamamlandığını açıkça göreceksiniz. Bu hız farkı, çok çekirdekli işlemcilerin gerçek gücünün CPU bağlı görevlerde nasıl kullanılabileceğini gösterir. Ancak, süreçler arasında veri paylaşımı, iş parçacıklarına göre daha karmaşıktır; veriler genellikle pickle edilerek (serileştirilerek) süreçler arasında kopyalanır. Bu nedenle, büyük veri yapılarını sürekli olarak süreçler arasında geçirirken dikkatli olmak ve bu veri transferinin getireceği ek yükü göz önünde bulundurmak önemlidir.
ProcessPoolExecutor kullanırken, görevlerinizi mümkün olduğunca bağımsız hale getirin. Süreçler arası iletişim (IPC) ek yük getirir ve performansı düşürebilir. Eğer görevleriniz arasında yoğun veri paylaşımı gerekiyorsa, paylaşılan bellek (multiprocessing.shared_memory) veya kuyruklar (multiprocessing.Queue) gibi daha gelişmiş IPC mekanizmalarını araştırmanız gerekebilir.
İleri Düzey İpuçları ve En İyi Uygulamalar: Executor'lardan Maksimum Verim Almak
ThreadPoolExecutor ve ProcessPoolExecutor'ı etkili bir şekilde kullanmak, sadece temel API'lerini bilmekle kalmaz, aynı zamanda bazı ileri düzey ipuçları ve en iyi uygulamaları anlamayı da gerektirir. Bu bilgiler, uygulamalarınızın daha sağlam, performanslı ve bakımı kolay olmasını sağlayacaktır.
Executor Seçimi: Doğru Aracı Kullanmak
En kritik kararlardan biri, hangi Executor'ı kullanacağınıza karar vermektir:
-
ThreadPoolExecutor(I/O Bağlı):Ağ istekleri, dosya okuma/yazma, veritabanı sorguları gibi G/Ç yoğun işlemleri için idealdir. GIL, I/O beklerken serbest bırakıldığından, iş parçacıkları bu süreyi etkin bir şekilde kullanarak diğer G/Ç işlemlerini başlatabilir. Bu, uygulamanın genel yanıt verme hızını artırır.
-
ProcessPoolExecutor(CPU Bağlı):Yoğun matematiksel hesaplamalar, görüntü/video işleme, büyük veri analizi gibi CPU yoğun işlemler için kullanılır. Her süreç kendi Python yorumlayıcısına sahip olduğu için GIL kısıtlamasını aşar ve çok çekirdekli işlemcilerde gerçek paralelizm sağlar. Ancak, süreçlerin başlatılması ve sonlandırılması iş parçacıklarına göre daha fazla kaynak tüketir ve süreçler arası iletişim (IPC) daha karmaşıktır.
Kural olarak: Eğer göreviniz zamanının çoğunu bir şeyi bekleyerek (I/O) geçiriyorsa, ThreadPoolExecutor kullanın. Eğer göreviniz zamanının çoğunu hesaplama yaparak (CPU) geçiriyorsa, ProcessPoolExecutor kullanın.
Gelecek Objeleri (Future Objects) ve Hata Yönetimi
executor.submit() metodu, görevin sonucunu doğrudan döndürmek yerine bir Future objesi döndürür. Bu obje, görevin tamamlanma durumunu izlemenize, sonucuna erişmenize veya herhangi bir hatayı yakalamanıza olanak tanır. future.result() metodu, görevin sonucunu döndürür; eğer görev henüz tamamlanmadıysa bekler, eğer görevde bir istisna oluştuysa bu istisnayı yeniden fırlatır. Bu nedenle, hata yönetimi için her zaman try-except bloklarını kullanmalısınız.
from concurrent.futures import ThreadPoolExecutor
import time
def risky_task(x):
if x % 2 == 0:
time.sleep(0.1) # Simüle edilmiş bir I/O işlemi
return x * x
else:
raise ValueError(f"{x} tek sayı, hata oluştu!")
with ThreadPoolExecutor(max_workers=3) as executor:
futures = [executor.submit(risky_task, i) for i in range(5)]
for future in futures:
try:
result = future.result()
print(f"Görev başarıyla tamamlandı, sonuç: {result}")
except ValueError as e:
print(f"Görevde hata oluştu: {e}")
except Exception as e:
print(f"Beklenmeyen bir hata oluştu: {e}")
Bu örnek, Future objelerinin nasıl izleneceğini ve hataların nasıl yönetileceğini gösterir. future.done(), future.cancelled() gibi metodları da kullanarak görevin durumunu sorgulayabilirsiniz.
as_completed ve wait Fonksiyonları
concurrent.futures modülü, birden fazla Future objesini yönetmek için iki kullanışlı fonksiyon sunar:
-
concurrent.futures.as_completed(futures):Verilen
Futureobjelerini, tamamlanma sırasına göre bir yineleyici olarak döndürür. Bu, görevlerin sonuçlarını geldikçe işlemek istediğinizde idealdir. Önce tamamlanan görevin sonucunu en erken alırsınız. -
concurrent.futures.wait(futures, timeout=None, return_when=ALL_COMPLETED):Belirtilen tüm
Futureobjelerinin tamamlanmasını veya belirli bir koşulun (örneğin, ilk tamamlanan, ilk hata veren) karşılanmasını bekler.return_whenparametresi, ne zaman geri döneceğini belirler (FIRST_COMPLETED,FIRST_EXCEPTION,ALL_COMPLETED). Tamamlanan ve tamamlanmayanFutureobjelerini içeren iki set döndürür. Görevlerin belirli bir zaman dilimi içinde bitmesini beklemek veya toplu işlem yapmak istediğinizde kullanışlıdır.
Kaynak Yönetimi ve Kapanış
Executor'ları with ifadesiyle kullanmak en iyi uygulamadır. Bu, Executor'ın işini bitirdiğinde havuzdaki tüm iş parçacıklarının/süreçlerinin düzgün bir şekilde kapatılmasını ve kaynakların serbest bırakılmasını garanti eder. Aksi takdirde, arka planda çalışan iş parçacıkları veya süreçler uygulamanız kapanana kadar yaşamaya devam edebilir ve kaynak sızıntılarına neden olabilir.
Daha Büyük Resim: Asyncio ile Entegrasyon
Executor'lar, özellikle I/O bağlı görevler için asyncio ile de entegre edilebilir. asyncio, tek bir iş parçacığında eşzamanlı G/Ç işlemlerini yönetmek için olay döngüsü (event loop) kullanan asenkron bir kütüphanedir. Eğer asenkron bir ortamda engelleme yapan (senkron) kod çalıştırmanız gerekiyorsa, loop.run_in_executor() fonksiyonunu kullanarak bu engelleme yapan görevi bir ThreadPoolExecutor'a devredebilir ve ana olay döngüsünün kesintiye uğramamasını sağlayabilirsiniz. Bu, modern asenkron Python uygulamalarında güçlü bir hibrid yaklaşım sunar.
Mobil Uyumlu Tasarım ve Performans İçgörüleri: Ekran Boyutuna Göre Veri İşleme
Python'daki Executor'lar doğrudan bir web sayfasının arayüz tasarımını kontrol etmese de, geliştirdiğimiz arka uç servislerin veya veri işleme mantığının, nihai kullanıcı arayüzünün (mobil veya masaüstü) gereksinimlerine göre adapte olması önemlidir. Örneğin, bir mobil uygulama küçük ekranlarda daha az veri göstermek veya daha az yoğun işlem gerektiren formatlarda veri talep etmek isteyebilirken, bir masaüstü uygulaması tam detaylı veri ve karmaşık görselleştirmeler isteyebilir. Bu, arka uçta çalışan Executor'ların veri hazırlama veya işleme süreçlerini nasıl etkileyebileceğine dair bir bağlam sunar.
Bir web uygulamasında, kullanıcıların cihazlarına göre değişen içerik veya performans optimizasyonları sunmak için genellikle CSS Media Query'leri kullanılır. Bu, front-end tarafında sayfanın nasıl görüneceğini belirlerken, arka planda çalışan Python uygulamamızın da bu farklı ihtiyaçlara göre veri sağlaması veya işlemesi gerekebilir. Örneğin, mobil bir kullanıcıdan gelen bir istek için daha küçük boyutlu resimler üretmek veya daha az detaylı raporlar hazırlamak gibi. Bu tür senaryolarda, ProcessPoolExecutor ile görüntü boyutlandırma veya ThreadPoolExecutor ile API çağrılarında parametre optimizasyonu gibi teknikler devreye girebilir.
Aşağıdaki HTML ve CSS kodu, bir web sayfasının (örneğin bu makalenin yayınlandığı bir blog) farklı ekran boyutlarına nasıl tepki verdiğini gösteren basit bir medya sorgusu örneğidir. Bu, Python arka ucunuzun farklı cihazlardan gelen talepleri nasıl farklılaştırması gerektiğini anlamanıza yardımcı olabilir. Python kodu doğrudan medya sorguları yazmaz, ancak web sunucusu (örneğin Flask veya Django ile yazılmış) aracılığıyla bu tür istemci bilgilerini (HTTP User-Agent başlığı gibi) alabilir ve işlemci seçimini veya veri hazırlığını buna göre optimize edebilir.
/* Bu bir CSS kod örneğidir, Python kodu değildir.
Ancak bir Python web uygulamasının çıktılarını
mobil uyumlu hale getirmek için kullanılan prensipleri gösterir. */
body {
font-family: Arial, sans-serif;
margin: 20px;
line-height: 1.6;
}
.content-wrapper {
max-width: 960px;
margin: 0 auto;
padding: 15px;
background-color: #f9f9f9;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.section-title {
color: #2c3e50;
margin-bottom: 10px;
}
/* Masaüstü ve büyük ekranlar için varsayılan stil */
.info-box {
background-color: #e8f5e9; /* Açık yeşil */
border-left: 5px solid #4CAF50;
padding: 10px;
margin: 15px 0;
font-size: 1.0em;
color: #388E3C;
}
/* Mobil cihazlar için medya sorgusu (ekran genişliği 768px veya daha küçük) */
@media screen and (max-width: 768px) {
body {
margin: 10px;
}
.content-wrapper {
padding: 10px;
border-radius: 0; /* Mobil görünümde köşeleri keskinleştir */
box-shadow: none; /* Mobil görünümde gölgeyi kaldır */
}
.section-title {
font-size: 1.3em;
}
.info-box {
font-size: 0.9em; /* Mobil için daha küçük font boyutu */
border-left: 3px solid #FFC107; /* Mobil için farklı renk */
background-color: #fffde7; /* Mobil için farklı arka plan */
margin: 10px 0;
}
}
/* Çok küçük mobil cihazlar için medya sorgusu (ekran genişliği 480px veya daha küçük) */
@media screen and (max-width: 480px) {
.section-title {
font-size: 1.1em;
}
.info-box {
padding: 8px;
font-size: 0.8em;
border-left: 2px solid #F44336; /* Daha küçük mobil için farklı renk */
background-color: #ffebee; /* Daha küçük mobil için farklı arka plan */
}
}
Bu CSS medya sorguları, bir web sayfasının "info-box" gibi belirli öğelerinin, ekran boyutuna göre nasıl farklı renk, yazı tipi boyutu veya kenarlık stilini alacağını gösterir. Python arka ucunuz, bu tür ön uç davranışlarını tamamlayacak şekilde veri işleme stratejilerini optimize edebilir. Örneğin, bir mobil cihazdan gelen resim yükleme isteğini algılayarak, ProcessPoolExecutor kullanarak resmi daha küçük çözünürlüklerde işleyebilir ve böylece mobil kullanıcının bant genişliğinden tasarruf etmesini sağlayabilir. Bu tür entegre bir yaklaşım, genel uygulama performansını ve kullanıcı deneyimini önemli ölçüde artırabilir.
Sonuç: Python Uygulamalarınızda Engellemeyen Güç
Python'ın Executor'ları, uygulamalarınızın performansını ve yanıt verme hızını artırmak için elinizdeki en güçlü araçlardan biridir. İster web sitelerinden veri çeken I/O yoğun bir uygulama geliştiriyor olun, ister karmaşık görüntü işleme yapan CPU yoğun bir sistem tasarlayın, ThreadPoolExecutor ve ProcessPoolExecutor doğru kullanımda mucizeler yaratabilir. Bu makalede, engellemeyen kodun temel prensiplerinden başlayarak, GIL'in etkilerini anladık ve her iki Executor türünü de gerçek dünya senaryolarıyla adım adım inceledik. Ayrıca, Future objeleriyle hata yönetimi, as_completed ve wait gibi ileri düzey fonksiyonlar ve Executor seçiminde en iyi uygulamalar hakkında önemli ipuçları edindik.
Unutmayın ki Executor'ların gücü, probleminizi doğru tanımlamanızda ve uygun Executor'ı seçmenizde yatar. I/O bağlı görevler için iş parçacıkları (threads), CPU bağlı görevler içinse süreçler (processes) en iyi sonucu verecektir. with ifadesi kullanarak kaynak yönetimini sağlamak, gelecekteki olası sorunların önüne geçer. Ayrıca, asyncio gibi asenkron kütüphanelerle entegrasyon, daha karmaşık ve yüksek performanslı sistemler için kapıları açar. Uygulamalarınızı daha duyarlı, verimli ve ölçeklenebilir hale getirmek için bu güçlü araçları projelerinize dahil etmekten çekinmeyin. Python'ın "gizli yaşamını" keşfetmek, geliştirme deneyiminizi zenginleştirecek ve kullanıcılarınıza daha iyi hizmet sunmanızı sağlayacaktır.
Sıkça Sorulan Sorular (SSS)
-
S: Python'da
threadingmi yoksaThreadPoolExecutormı kullanmalıyım?C: Genel olarak
ThreadPoolExecutorkullanmanız tavsiye edilir.ThreadPoolExecutor, iş parçacığı yönetimi, havuzlama ve hata işleme gibi detayları sizin için soyutlar, daha temiz ve okunabilir kod yazmanızı sağlar.threadingmodülü daha düşük seviyeli kontrol sunar ancak genellikleconcurrent.futuresmodülünün sunduğu yüksek seviyeli soyutlama çoğu senaryo için yeterlidir ve kullanımı daha kolaydır. -
S:
max_workersdeğeri ne olmalı?C:
ThreadPoolExecutor(I/O bağlı) içinmax_workersgenellikle işlemci çekirdeği sayısından çok daha fazla olabilir (örneğin 32 veya 64), çünkü iş parçacıkları I/O beklerken GIL'i serbest bırakır.ProcessPoolExecutor(CPU bağlı) için isemax_workersgenellikleos.cpu_count()ile alınan CPU çekirdeği sayısına eşit veya biraz daha fazla (genellikleos.cpu_count() + 1) ayarlanır, zira daha fazla süreç açmak genellikle ek yük getirir ve performansı düşürür. -
S: GIL, Python'da gerçek paralelizmi tamamen mi engeller?
C: Evet, GIL, *aynı anda* birden fazla iş parçacığının Python bytecode'unu yürütmesini engeller. Bu nedenle
threadingveyaThreadPoolExecutorCPU bağlı görevlerde gerçek paralelizm sağlamaz. Ancak I/O bağlı görevlerde bir iş parçacığı I/O beklerken GIL'i serbest bırakabildiği için eşzamanlılık (concurrency) sağlanır ve bu da performansı artırır. Gerçek paralelizm içinProcessPoolExecutorveyamultiprocessingmodülü kullanılmalıdır. -
S: Executor'lar ve
asyncioarasındaki fark nedir? Ne zaman hangisini kullanmalıyım?C:
asyncio, tek bir iş parçacığı üzerinde çalışan ve I/O bağlı görevler için optimize edilmiş eşzamanlı bir yapıdır. Olay döngüsü (event loop) kullanarak görevler arasında hızlıca geçiş yapar. Executor'lar ise iş parçacıkları veya süreçler kullanarak paralelizm veya eşzamanlılık sağlar.
asyncio'yu, tamamen asenkron olarak tasarlanmış (awaitanahtar kelimesiyle işaretlenmiş) I/O bağlı görevleriniz olduğunda kullanmalısınız. Engelleme yapan senkron I/O veya CPU bağlı kodunuz varsa, bunlarıasyncioolay döngüsünü engellememek içinloop.run_in_executor()aracılığıyla birThreadPoolExecutor'a veyaProcessPoolExecutor'a devretmelisiniz. -
S: Executor'lar ne zaman kullanılmamalıdır?
C: Eğer görevleriniz çok kısa süreli ve çok sayıda ise, iş parçacığı/süreç oluşturmanın ve yönetmenin getirdiği ek yük, elde edeceğiniz performanstan daha fazla olabilir. Bu durumlarda, basit döngüler veya daha hafif eşzamanlılık mekanizmaları (örneğin
asyncio'da doğrudan await edilebilir coroutine'ler) daha uygun olabilir. Ayrıca, süreçler arasında çok yoğun ve karmaşık veri paylaşımı gerektiren durumlardaProcessPoolExecutoryerine daha spesifik IPC mekanizmalarına (mesaj kuyrukları, paylaşılan bellek) veya doğrudanmultiprocessingmodülüne yönelmek daha mantıklı olabilir.