Python'daki concurrent.futures modülüne kapsamlı bir rehber. Paralel görev yürütme için ThreadPoolExecutor ve ProcessPoolExecutor'ü karşılaştırır, pratik örnekler sunar.
Python'da Eşzamanlılığı Serbest Bırakmak: ThreadPoolExecutor ve ProcessPoolExecutor Karşılaştırması
Python, çok yönlü ve yaygın olarak kullanılan bir programlama dili olmasına rağmen, Global Interpreter Lock (GIL) nedeniyle gerçek paralellik söz konusu olduğunda bazı sınırlamalara sahiptir. concurrent.futures
modülü, çağrılabilir nesneleri eşzamansız olarak yürütmek için üst düzey bir arayüz sağlar ve bu sınırlamaların bazılarını aşmanın ve belirli görev türleri için performansı iyileştirmenin bir yolunu sunar. Bu modül iki temel sınıf sağlar: ThreadPoolExecutor
ve ProcessPoolExecutor
. Bu kapsamlı kılavuz, her ikisini de keşfedecek, farklılıklarını, güçlü ve zayıf yönlerini vurgulayacak ve ihtiyaçlarınız için doğru yürütücüyü seçmenize yardımcı olacak pratik örnekler sunacaktır.
Eşzamanlılığı ve Paralelliği Anlamak
Her bir yürütücünün özelliklerine dalmadan önce, eşzamanlılık ve paralellik kavramlarını anlamak çok önemlidir. Bu terimler genellikle birbirinin yerine kullanılır, ancak farklı anlamlara sahiptir:
- Eşzamanlılık: Aynı anda birden çok görevi yönetmekle ilgilenir. Kodunuzu görünüşte aynı anda birden çok şeyi işleyecek şekilde yapılandırmakla ilgilidir, hatta bunlar aslında tek bir işlemci çekirdeğinde birbirine geçse bile. Bunu tek bir ocakta birkaç tencereyi yöneten bir şef olarak düşünün - hepsi *tam olarak* aynı anda kaynamıyor, ancak şef hepsini yönetiyor.
- Paralellik: Birden çok görevi *aynı* anda, tipik olarak birden çok işlemci çekirdeği kullanarak gerçekte yürütmeyi içerir. Bu, her biri yemeğin farklı bir bölümünde aynı anda çalışan birden çok şefe sahip olmak gibidir.
Python'un GIL'i, iş parçacıklarını kullanırken CPU-bağlı görevler için büyük ölçüde gerçek paralelliği engeller. Bunun nedeni, GIL'in herhangi bir zamanda yalnızca bir iş parçacığının Python yorumlayıcısının kontrolünü elinde tutmasına izin vermesidir. Ancak, programın zamanının çoğunu ağ istekleri veya disk okumaları gibi harici işlemleri bekleyerek geçirdiği G/Ç-bağlı görevler için, bir iş parçacığı beklerken diğer iş parçacıklarının çalışmasına izin vererek iş parçacıkları yine de önemli performans iyileştirmeleri sağlayabilir.
`concurrent.futures` Modülünü Tanıtıyoruz
concurrent.futures
modülü, görevleri eşzamansız olarak yürütme sürecini basitleştirir. İş parçacıkları ve süreçlerle çalışmak için üst düzey bir arayüz sağlar ve bunları doğrudan yönetmekle ilgili karmaşıklığın çoğunu soyutlar. Temel kavram, gönderilen görevlerin yürütülmesini yöneten "yürütücü"dür. İki temel yürütücü şunlardır:
ThreadPoolExecutor
: Görevleri yürütmek için bir iş parçacığı havuzu kullanır. G/Ç-bağlı görevler için uygundur.ProcessPoolExecutor
: Görevleri yürütmek için bir süreç havuzu kullanır. CPU-bağlı görevler için uygundur.
ThreadPoolExecutor: G/Ç-Bağlı Görevler için İş Parçacıklarından Yararlanma
ThreadPoolExecutor
, görevleri yürütmek için bir çalışan iş parçacığı havuzu oluşturur. GIL nedeniyle, iş parçacıkları gerçek paralellikten yararlanan hesaplama açısından yoğun işlemler için ideal değildir. Ancak, G/Ç-bağlı senaryolarda başarılı olurlar. Nasıl kullanılacağını keşfedelim:
Temel Kullanım
İşte birden çok web sayfasını eşzamanlı olarak indirmek için ThreadPoolExecutor
kullanmanın basit bir örneği:
import concurrent.futures
import requests
import time
urls = [
"https://www.example.com",
"https://www.google.com",
"https://www.wikipedia.org",
"https://www.python.org"
]
def download_page(url):
try:
response = requests.get(url, timeout=5)
response.raise_for_status() # Kötü yanıtlar için HTTPError oluştur (4xx veya 5xx)
print(f"İndirilen {url}: {len(response.content)} bayt")
return len(response.content)
except requests.exceptions.RequestException as e:
print(f"İndirme hatası {url}: {e}")
return 0
start_time = time.time()
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
# Her URL'yi yürütücüye gönder
futures = [executor.submit(download_page, url) for url in urls]
# Tüm görevlerin tamamlanmasını bekle
total_bytes = sum(future.result() for future in concurrent.futures.as_completed(futures))
print(f"Toplam indirilen bayt: {total_bytes}")
print(f"Geçen süre: {time.time() - start_time:.2f} saniye")
Açıklama:
- Gerekli modülleri içe aktarıyoruz:
concurrent.futures
,requests
vetime
. - İndirilecek URL'lerin bir listesini tanımlıyoruz.
download_page
işlevi, verilen bir URL'nin içeriğini alır. Olası ağ sorunlarını yakalamak için `try...except` ve `response.raise_for_status()` kullanılarak hata işleme dahil edilmiştir.- En fazla 4 çalışan iş parçacığına sahip bir
ThreadPoolExecutor
oluşturuyoruz.max_workers
bağımsız değişkeni, aynı anda kullanılabilecek iş parçacığı sayısını kontrol eder. Çok yüksek ayarlamak, özellikle ağ bant genişliğinin genellikle darboğaz olduğu G/Ç'ye bağlı görevlerde her zaman performansı iyileştirmeyebilir. executor.submit(download_page, url)
kullanarak her URL'yi yürütücüye göndermek için bir liste kavraması kullanıyoruz. Bu, her görev için birFuture
nesnesi döndürür.concurrent.futures.as_completed(futures)
işlevi, tamamlandıkça futures veren bir yineleyici döndürür. Bu, sonuçları işlemeden önce tüm görevlerin bitmesini beklemekten kaçınır.- Tamamlanan futures boyunca yineleme yapıyoruz ve toplam indirilen baytları toplayarak
future.result()
kullanarak her görevin sonucunu alıyoruz. `download_page` içindeki hata işleme, bireysel hataların tüm işlemi çökertmemesini sağlar. - Son olarak, toplam indirilen baytları ve geçen süreyi yazdırıyoruz.
ThreadPoolExecutor'un Faydaları
- Basitleştirilmiş Eşzamanlılık: İş parçacıklarını yönetmek için temiz ve kullanımı kolay bir arayüz sağlar.
- G/Ç-Bağlı Performans: Ağ istekleri, dosya okumaları veya veritabanı sorguları gibi G/Ç işlemlerini bekleyerek önemli miktarda zaman harcayan görevler için mükemmeldir.
- Azaltılmış Yük: İş parçacıkları genellikle süreçlere göre daha düşük yüke sahiptir, bu da onları sık sık bağlam geçişi içeren görevler için daha verimli hale getirir.
ThreadPoolExecutor'un Sınırlamaları
- GIL Kısıtlaması: GIL, CPU-bağlı görevler için gerçek paralelliği sınırlar. Her seferinde yalnızca bir iş parçacığı Python bayt kodunu yürütebilir, bu da birden çok çekirdeğin faydalarını ortadan kaldırır.
- Hata Ayıklama Karmaşıklığı: Çok iş parçacıklı uygulamalarda hata ayıklamak, yarış koşulları ve diğer eşzamanlılıkla ilgili sorunlar nedeniyle zor olabilir.
ProcessPoolExecutor: CPU-Bağlı Görevler için Çoklu İşlemi Serbest Bırakmak
ProcessPoolExecutor
, bir çalışan süreç havuzu oluşturarak GIL sınırlamasının üstesinden gelir. Her işlemin kendi Python yorumlayıcısı ve bellek alanı vardır ve çok çekirdekli sistemlerde gerçek paralelliğe olanak tanır. Bu, onu ağır hesaplamalar içeren CPU-bağlı görevler için ideal hale getirir.
Temel Kullanım
Büyük bir sayı aralığı için karelerin toplamını hesaplamak gibi hesaplama açısından yoğun bir görevi düşünün. Bu görevi paralelleştirmek için ProcessPoolExecutor
şu şekilde kullanılır:
import concurrent.futures
import time
import os
def sum_of_squares(start, end):
pid = os.getpid()
print(f"İşlem Kimliği: {pid}, {start} ile {end} arasındaki karelerin toplamı hesaplanıyor")
total = 0
for i in range(start, end + 1):
total += i * i
return total
if __name__ == "__main__": #Bazı ortamlarda özyinelemeli üretimi önlemek için önemlidir
start_time = time.time()
range_size = 1000000
num_processes = 4
ranges = [(i * range_size + 1, (i + 1) * range_size) for i in range(num_processes)]
with concurrent.futures.ProcessPoolExecutor(max_workers=num_processes) as executor:
futures = [executor.submit(sum_of_squares, start, end) for start, end in ranges]
results = [future.result() for future in concurrent.futures.as_completed(futures)]
total_sum = sum(results)
print(f"Karelerin toplam toplamı: {total_sum}")
print(f"Geçen süre: {time.time() - start_time:.2f} saniye")
Açıklama:
- Verilen bir sayı aralığı için karelerin toplamını hesaplayan
sum_of_squares
işlevini tanımlıyoruz. Her aralığı hangi işlemin yürüttüğünü görmek için `os.getpid()`'yi ekliyoruz. - Kullanılacak aralık boyutunu ve işlem sayısını tanımlıyoruz.
ranges
listesi, toplam hesaplama aralığını her işlem için bir tane olmak üzere daha küçük parçalara bölmek için oluşturulur. - Belirtilen sayıda çalışan işlemle bir
ProcessPoolExecutor
oluşturuyoruz. executor.submit(sum_of_squares, start, end)
kullanarak her aralığı yürütücüye gönderiyoruz.future.result()
kullanarak her future'dan sonuçları topluyoruz.- Nihai toplamı almak için tüm işlemlerden gelen sonuçları topluyoruz.
Önemli Not: Özellikle Windows'ta ProcessPoolExecutor
kullanırken, yürütücüyü oluşturan kodu bir if __name__ == "__main__":
bloğu içine almalısınız. Bu, hatalara ve beklenmedik davranışlara yol açabilecek özyinelemeli işlem üretimini önler. Bunun nedeni, modülün her alt işlemde yeniden içe aktarılmasıdır.
ProcessPoolExecutor'un Faydaları
- Gerçek Paralellik: GIL sınırlamasının üstesinden gelir ve CPU-bağlı görevler için çok çekirdekli sistemlerde gerçek paralelliğe olanak tanır.
- CPU-Bağlı Görevler için Geliştirilmiş Performans: Hesaplama açısından yoğun işlemler için önemli performans kazanımları elde edilebilir.
- Sağlamlık: Bir işlem çökerse, işlemler birbirinden yalıtılmış olduğundan programın tamamını mutlaka çökertmez.
ProcessPoolExecutor'un Sınırlamaları
- Daha Yüksek Yük: Süreçler oluşturmak ve yönetmek, iş parçacıklarına kıyasla daha yüksek yüke sahiptir.
- Süreçler Arası İletişim: Süreçler arasında veri paylaşmak daha karmaşık olabilir ve ek yüke neden olabilecek süreçler arası iletişim (IPC) mekanizmaları gerektirir.
- Bellek Ayak İzi: Her işlemin kendi bellek alanı vardır, bu da uygulamanın genel bellek ayak izini artırabilir. Süreçler arasında büyük miktarda veri geçirmek bir darboğaz haline gelebilir.
Doğru Yürütücüyü Seçmek: ThreadPoolExecutor ve ProcessPoolExecutor
ThreadPoolExecutor
ve ProcessPoolExecutor
arasında seçim yapmanın anahtarı, görevlerinizin doğasını anlamaktır:
- G/Ç-Bağlı Görevler: Görevleriniz zamanlarının çoğunu G/Ç işlemlerini (örneğin, ağ istekleri, dosya okumaları, veritabanı sorguları) bekleyerek geçiriyorsa,
ThreadPoolExecutor
genellikle daha iyi bir seçimdir. GIL bu senaryolarda daha az darboğazdır ve iş parçacıklarının daha düşük yükü, onları daha verimli hale getirir. - CPU-Bağlı Görevler: Görevleriniz hesaplama açısından yoğunsa ve birden çok çekirdek kullanıyorsa,
ProcessPoolExecutor
kullanmanız gerekir. GIL sınırlamasını atlar ve gerçek paralelliğe olanak tanır, bu da önemli performans iyileştirmeleri sağlar.
İşte temel farklılıkları özetleyen bir tablo:
Özellik | ThreadPoolExecutor | ProcessPoolExecutor |
---|---|---|
Eşzamanlılık Modeli | Çoklu İş Parçacığı | Çoklu İşlem |
GIL Etkisi | GIL ile sınırlı | GIL'i atlar |
Uygun olduğu yer | G/Ç-bağlı görevler | CPU-bağlı görevler |
Yük | Daha düşük | Daha yüksek |
Bellek Ayak İzi | Daha düşük | Daha yüksek |
Süreçler Arası İletişim | Gerekli değil (iş parçacıkları belleği paylaşır) | Veri paylaşımı için gerekli |
Sağlamlık | Daha az sağlam (bir çökme tüm işlemi etkileyebilir) | Daha sağlam (işlemler yalıtılmıştır) |
Gelişmiş Teknikler ve Dikkat Edilmesi Gerekenler
Bağımsız Değişkenlerle Görevleri Gönderme
Her iki yürütücü de yürütülen işleve bağımsız değişkenler geçirmenize olanak tanır. Bu, submit()
yöntemi aracılığıyla yapılır:
with concurrent.futures.ThreadPoolExecutor() as executor:
future = executor.submit(my_function, arg1, arg2)
result = future.result()
Özel Durumları İşleme
Yürütülen işlev içinde oluşan özel durumlar otomatik olarak ana iş parçacığına veya işleme yayılmaz. Future
'ın sonucunu alırken bunları açıkça işlemeniz gerekir:
with concurrent.futures.ThreadPoolExecutor() as executor:
future = executor.submit(my_function)
try:
result = future.result()
except Exception as e:
print(f"Bir özel durum oluştu: {e}")
Basit Görevler için `map` Kullanma
Aynı işlevi bir girdi dizisine uygulamak istediğiniz basit görevler için, map()
yöntemi görevleri göndermenin kısa ve öz bir yolunu sağlar:
def square(x):
return x * x
with concurrent.futures.ProcessPoolExecutor() as executor:
numbers = [1, 2, 3, 4, 5]
results = executor.map(square, numbers)
print(list(results))
Çalışan Sayısını Kontrol Etme
Hem ThreadPoolExecutor
hem de ProcessPoolExecutor
'daki max_workers
bağımsız değişkeni, aynı anda kullanılabilecek maksimum iş parçacığı veya işlem sayısını kontrol eder. max_workers
için doğru değeri seçmek performans için önemlidir. İyi bir başlangıç noktası, sisteminizde bulunan CPU çekirdeği sayısıdır. Ancak, G/Ç'ye bağlı görevler için, iş parçacıkları G/Ç'yi beklerken diğer görevlere geçebileceğinden, çekirdeklerden daha fazla iş parçacığı kullanmaktan yararlanabilirsiniz. Optimum değeri belirlemek için genellikle deneme ve profil oluşturma gerekir.
İlerlemeyi İzleme
concurrent.futures
modülü, görevlerin ilerlemesini doğrudan izlemek için yerleşik mekanizmalar sağlamaz. Ancak, geri aramalar veya paylaşılan değişkenler kullanarak kendi ilerleme takibinizi uygulayabilirsiniz. İlerleme çubuklarını görüntülemek için `tqdm` gibi kitaplıklar entegre edilebilir.
Gerçek Dünya Örnekleri
ThreadPoolExecutor
ve ProcessPoolExecutor
'un etkili bir şekilde uygulanabileceği bazı gerçek dünya senaryolarını ele alalım:
- Web Kazıma:
ThreadPoolExecutor
kullanarak birden çok web sayfasını eşzamanlı olarak indirme ve ayrıştırma. Her iş parçacığı farklı bir web sayfasını işleyebilir, bu da genel kazıma hızını artırır. Web sitesi hizmet şartlarına dikkat edin ve sunucularını aşırı yüklemekten kaçının. - Görüntü İşleme:
ProcessPoolExecutor
kullanarak geniş bir görüntü kümesine görüntü filtreleri veya dönüşümleri uygulama. Her işlem farklı bir görüntüyü işleyebilir, bu da daha hızlı işleme için birden çok çekirdekten yararlanır. Verimli görüntü işleme için OpenCV gibi kitaplıkları düşünün. - Veri Analizi:
ProcessPoolExecutor
kullanarak büyük veri kümeleri üzerinde karmaşık hesaplamalar yapma. Her işlem verilerin bir alt kümesini analiz edebilir, bu da genel analiz süresini kısaltır. Pandas ve NumPy, Python'da veri analizi için popüler kitaplıklardır. - Makine Öğrenimi:
ProcessPoolExecutor
kullanarak makine öğrenimi modellerini eğitme. Bazı makine öğrenimi algoritmaları etkili bir şekilde paralelleştirilebilir ve bu da daha hızlı eğitim sürelerine olanak tanır. Scikit-learn ve TensorFlow gibi kitaplıklar paralelleştirme için destek sunar. - Video Kodlama:
ProcessPoolExecutor
kullanarak video dosyalarını farklı biçimlere dönüştürme. Her işlem farklı bir video segmentini kodlayabilir, bu da genel kodlama sürecini hızlandırır.
Küresel Hususlar
Küresel bir kitle için eşzamanlı uygulamalar geliştirirken, aşağıdakileri göz önünde bulundurmak önemlidir:
- Saat Dilimleri: Zamana duyarlı işlemlerle uğraşırken saat dilimlerine dikkat edin. Saat dilimi dönüştürmelerini işlemek için
pytz
gibi kitaplıklar kullanın. - Yerel Ayarlar: Uygulamanızın farklı yerel ayarları doğru şekilde işlediğinden emin olun. Sayıları, tarihleri ve para birimlerini kullanıcının yerel ayarına göre biçimlendirmek için
locale
gibi kitaplıklar kullanın. - Karakter Kodlamaları: Geniş bir dil yelpazesini desteklemek için varsayılan karakter kodlaması olarak Unicode (UTF-8) kullanın.
- Uluslararasılaştırma (i18n) ve Yerelleştirme (l10n): Uygulamanızı kolayca uluslararasılaştırılacak ve yerelleştirilecek şekilde tasarlayın. Farklı diller için çeviriler sağlamak için gettext veya diğer çeviri kitaplıklarını kullanın.
- Ağ Gecikmesi: Uzak hizmetlerle iletişim kurarken ağ gecikmesini göz önünde bulundurun. Uygulamanızın ağ sorunlarına karşı dayanıklı olmasını sağlamak için uygun zaman aşımları ve hata işleme uygulayın. Sunucuların coğrafi konumu gecikmeyi önemli ölçüde etkileyebilir. Farklı bölgelerdeki kullanıcılar için performansı iyileştirmek için İçerik Dağıtım Ağlarını (CDN'ler) kullanmayı düşünün.
Sonuç
concurrent.futures
modülü, Python uygulamalarınıza eşzamanlılık ve paralellik getirmek için güçlü ve kullanışlı bir yol sağlar. ThreadPoolExecutor
ve ProcessPoolExecutor
arasındaki farklılıkları anlayarak ve görevlerinizin doğasını dikkatlice değerlendirerek, kodunuzun performansını ve yanıt verme hızını önemli ölçüde iyileştirebilirsiniz. Belirli kullanım durumunuz için en uygun ayarları bulmak için kodunuzu profil oluşturmayı ve farklı yapılandırmalarla denemeyi unutmayın. Ayrıca, GIL'in sınırlamalarının ve çok iş parçacıklı ve çoklu işlem programlamanın potansiyel karmaşıklıklarının farkında olun. Dikkatli planlama ve uygulama ile Python'da eşzamanlılığın tüm potansiyelini ortaya çıkarabilir ve küresel bir kitle için sağlam ve ölçeklenebilir uygulamalar oluşturabilirsiniz.