15%

Tüm Hosting Hizmetlerinde %15 indirim

Becerilerini test et ve herhangi bir hosting planında İndirim kazan

Kodu kullanın:

Skills
Başlayın
09.10.2024

Python Multiprocessing: Paralel Yürütme için Eksiksiz Teknik Rehber

Python’ın multiprocessing modülü, her biri kendi bellek alanına ve Python yorumlayıcısına sahip bağımsız işletim sistemi düzeyinde süreçler oluşturarak gerçek paralel yürütmeyi mümkün kılar — Global Interpreter Lock (GIL)‘i tamamen devre dışı bırakır. Tek bir yorumlayıcı durumunu paylaşan ve GIL tarafından serileştirilen iş parçacıklarının aksine, ayrı süreçler mevcut tüm CPU çekirdeklerinde eş zamanlı olarak çalışır; bu da multiprocessing’i sayısal hesaplama, görüntü işleme ve makine öğrenmesi çıkarımı gibi CPU’ya bağlı iş yükleri için doğru araç haline getirir.

Bu kılavuz, Python’ın süreç modelinin temel mimarisinden paylaşılan bellek, süreç havuzları, süreçler arası iletişim ve çoğu öğreticinin tamamen atladığı üretim düzeyindeki tuzaklar dahil olmak üzere gelişmiş kalıplara kadar her şeyi kapsamaktadır.

GIL’in Çoklu İş Parçacığını CPU’ya Bağlı İşler İçin Neden Yetersiz Kıldığı

Global Interpreter Lock, CPython’ın dahili nesne referans sayılarını koruyan bir mutex’tir. Herhangi bir anda yalnızca bir iş parçacığı GIL’i tutabilir ve Python bayt kodunu yürütebilir. Ağ istekleri, veritabanı sorguları, dosya okuma gibi G/Ç’ye bağlı görevler için iş parçacıkları kullanışlı olmaya devam eder; çünkü GIL, engelleyici G/Ç sistem çağrıları sırasında serbest bırakılır. Ancak saf hesaplama için iş parçacıkları GIL için sürekli rekabet eder ve 64 çekirdekli bir makinede bile gerçek bir paralellik sağlanamaz.

Multiprocessing bunu tamamen aşar. Oluşturulan her süreç, kendi CPython yorumlayıcısına, heap’ine ve GIL’ine sahip tam ve bağımsız bir işletim sistemi sürecidir. İşletim sistemi zamanlayıcısı bu süreçleri fiziksel çekirdeklere dağıtarak gerçek paralellik sağlar.

GIL Etkisi: Somut Bir Örnek

10 milyon tam sayı toplama işlemi gerçekleştiren bir fonksiyon düşünün. Çift çekirdekli bir makinede iki iş parçacığında çalıştırmak, tek bir iş parçacığında çalıştırmakla yaklaşık olarak aynı duvar saati süresini alır — bazen GIL çekişme ek yükü nedeniyle daha uzun sürer. İki ayrı süreçte çalıştırmak ise duvar saati süresini yarıya indirir.

Multiprocessing – Multithreading – Asyncio Karşılaştırması

Her eşzamanlılık modelinin ne zaman kullanılacağını anlamak, nasıl kullanılacağını bilmek kadar önemlidir.

Özellik`multiprocessing``threading``asyncio`
Paralellik türüGerçek (işletim sistemi süreçleri)Sözde (GIL sınırlı)İşbirlikçi (tek iş parçacıklı)
GIL atlatmaEvetHayırHayır
Bellek modeliSüreç başına ayrıPaylaşılanPaylaşılan
En iyi kullanım durumuCPU’ya bağlı görevlerG/Ç’ye bağlı + eski kütüphanelerG/Ç’ye bağlı, yüksek eşzamanlılık
İletişim ek yüküYüksek (IPC gerekli)Düşük (paylaşılan bellek)Düşük (coroutine’ler)
Hata yalıtımıGüçlü (çökme yalıtımı)Zayıf (bir iş parçacığı çökmesi hepsini öldürebilir)Zayıf
Başlatma ek yüküYüksekDüşükÇok düşük
Tipik bellek kullanımıYüksekDüşükÇok düşük

Genel kural: CPU’ya bağlı işler için `multiprocessing`, G/Ç’ye bağlı işler için `threading` veya `asyncio` kullanın. Her ikisine de ihtiyaç duyuyorsanız, `concurrent.futures` her iki model üzerinde birleşik bir arayüz sağlar.

Temel Mimari: Python Süreçleri Nasıl Oluşturur

Python, alt süreçler oluşturmak için üç başlatma yöntemi destekler ve seçim önemli sonuçlar doğurur:

  • `fork` (Linux/macOS’ta varsayılan): Üst süreç belleğini copy-on-write kullanarak kopyalar. Hızlıdır, ancak çok iş parçacıklı üst süreçlerde veya kilit tutan C uzantılarında sorunlara yol açabilir.
  • `spawn` (Windows’ta varsayılan, tüm platformlarda kullanılabilir): Yeni bir Python yorumlayıcısı başlatır ve modülü içe aktarır. Daha yavaş ama daha güvenlidir. Tüm kodun içe aktarılabilir olmasını gerektirir; bu nedenle `if __name__ == "__main__":` koruması zorunludur.
  • `forkserver`: Özel bir sunucu süreci talep üzerine fork eder. Fork güvenliği sorunlarını önler ve çok sayıda kısa ömürlü süreç için saf spawn’dan daha verimlidir.

Giriş noktanızın en üstünde başlatma yöntemini açıkça belirleyin:

“`python

import multiprocessing

if __name__ == "__main__":

multiprocessing.set_start_method("spawn")

“`

Başlatma yöntemlerini anlamamak, üretim multiprocessing kodunda platform’a özgü ince hataların en yaygın kaynaklarından biridir.

Modülü İçe Aktarma

“`python

import multiprocessing

from multiprocessing import Process, Pool, Queue, Lock, Pipe, Value, Array

“`

Temel Primitifler ve Rolleri

PrimitifAmaç
`Process`Tek bir bağımsız süreç oluşturur
`Pool`Yeniden kullanılabilir bir çalışan havuzunu yönetir
`Queue`IPC için iş parçacığı ve süreç güvenli FIFO
`Pipe`İki süreç arasında hızlı iki uçlu bağlantı
`Lock` / `RLock`Paylaşılan kaynaklar için karşılıklı dışlama
`Value` / `Array`Basit türler için paylaşılan bellek
`Manager`Karmaşık paylaşılan durum için proxy nesneleri
`Event` / `Semaphore`Senkronizasyon primitifleri

Örnek 1: Tek Bir Süreç Oluşturma

`Process` sınıfı temel yapı taşıdır. Doğrudan bir işletim sistemi süreciyle eşleşir.

“`python

from multiprocessing import Process

def compute_square(n):

result = n ** 2

print(f"Square of {n} is {result}")

if __name__ == "__main__":

process = Process(target=compute_square, args=(7,))

process.start()

process.join()

print(f"Process exit code: {process.exitcode}")

“`

Temel özellikler ve yöntemler:

  • `target`: Alt süreçte yürütülecek çağrılabilir nesne.
  • `args` / `kwargs`: Hedef fonksiyona iletilen argümanlar.
  • `start()`: Alt süreci fork eder veya oluşturur.
  • `join(timeout=None)`: Süreç sonlanana kadar çağıranı engeller. Zombie süreçleri önlemek için her zaman `join()` çağırın.
  • `exitcode`: Temiz çıkışta `0`, bir sinyal tarafından öldürüldüyse negatif değer, süreç işlenmemiş bir istisna fırlattıysa pozitif değer döndürür.
  • `is_alive()`: Süreç hâlâ çalışıyorsa `True` döndürür.
  • `terminate()` / `kill()`: Sırasıyla `SIGTERM` / `SIGKILL` gönderir. Dikkatli kullanın — kaynaklar temizlenmeyebilir.

Kritik tuzak: `join()` çağırmadan bir süreç oluşturursanız, Unix sistemlerinde alt süreç, üst süreç çıkana kadar bir süreç tablosu girişi tüketen bir zombie sürece dönüşür.

Örnek 2: `multiprocessing.Pool` ile Süreç Havuzları

Aynı fonksiyonu birçok veri öğesine uygulayan iş yükleri için `Pool`, tek tek `Process` örneklerini manuel olarak yönetmekten çok daha verimlidir. Sabit sayıda çalışan süreci korur ve işi bunlar arasında dağıtır.

“`python

from multiprocessing import Pool

import os

def process_chunk(data_chunk):

worker_pid = os.getpid()

result = sum(x ** 2 for x in data_chunk)

return result, worker_pid

if __name__ == "__main__":

dataset = [range(i, i + 1000) for i in range(0, 10000, 1000)]

with Pool(processes=4) as pool:

results = pool.map(process_chunk, dataset)

for result, pid in results:

print(f"Worker PID {pid} computed sum: {result}")

“`

Havuz Yöntemlerinin Karşılaştırması

YöntemEngelleyiciDöndürürEn İyi Kullanım
`pool.map(f, iterable)`EvetSonuç listesiBasit paralel map
`pool.imap(f, iterable)`TembelYineleyiciBüyük yinelenebilirler, bellek verimliliği
`pool.imap_unordered(f, iterable)`TembelYineleyici (sırasız)Sıranın önemi olmadığı durumlar
`pool.starmap(f, iterable)`EvetSonuç listesiBirden fazla argümanlı fonksiyonlar
`pool.apply_async(f, args)`Hayır`AsyncResult`Gönder-unut veya geri çağırmalar
`pool.map_async(f, iterable)`Hayır`AsyncResult`Engellemesiz toplu gönderim

Tuzak — havuz boyutu seçimi: `processes`’ı `os.cpu_count()`’dan yüksek ayarlamak, CPU’ya bağlı görevler için nadiren verimi artırır ve bağlam değiştirme ek yükünü artırır. Yaygın bir sezgisel kural, işletim sistemi ve ana süreç için bir çekirdek bırakmak amacıyla `processes = os.cpu_count() – 1` kullanmaktır.

Tuzak — serileştirme: Ana süreç ile çalışanlar arasında iletilen tüm argümanlar ve dönüş değerleri `pickle` kullanılarak serileştirilir. Pickle edilemeyen nesneler (lambda fonksiyonları, diğer fonksiyonların içinde tanımlanan iç içe fonksiyonlar, dosya tanıtıcıları, veritabanı bağlantıları) `PicklingError` hatası fırlatır. Modül düzeyindeki fonksiyonlarla `pool.starmap` kullanın veya pickle edilemeyen nesneleri iletmekten kaçınmak için kodunuzu yeniden yapılandırın.

Örnek 3: Queue ile Süreçler Arası İletişim

`multiprocessing.Queue`, bir pipe ve kilit üzerine inşa edilmiş süreç güvenli bir FIFO’dur. Üretici-tüketici kalıbı için standart mekanizmadır.

“`python

from multiprocessing import Process, Queue

import time

def producer(queue, items):

for item in items:

queue.put(item)

print(f"[Producer] Enqueued: {item}")

time.sleep(0.01)

queue.put(None) # Sentinel value to signal completion

def consumer(queue):

while True:

item = queue.get()

if item is None:

print("[Consumer] Received sentinel, shutting down.")

break

print(f"[Consumer] Processing: {item}")

if __name__ == "__main__":

q = Queue(maxsize=10) # Bounded queue prevents unbounded memory growth

data = list(range(20))

p = Process(target=producer, args=(q, data))

c = Process(target=consumer, args=(q,))

p.start()

c.start()

p.join()

c.join()

“`

Kritik tasarım notu: Tüketimi durdurmak için ne zaman duracağınızı belirlemek amacıyla asla `queue.empty()` kullanmayın. `empty()` kontrolü, çok işlemli bir bağlamda güvenilir değildir — kontrol ile ardından gelen `get()` arasında bir yarış koşulu mevcuttur. Üretimin tamamlandığını bildirmek için her zaman bir sentinel değeri (`None` veya özel bir `STOP` nesnesi gibi) kullanın.

Örnek 4: Value ve Array ile Paylaşılan Bellek

Süreçlerin bir `Queue`’nin ek yükü olmadan basit sayısal durumu paylaşması gerektiğinde, `multiprocessing.Value` ve `multiprocessing.Array`, `ctypes` tarafından desteklenen doğrudan paylaşılan bellek sağlar.

“`python

from multiprocessing import Process, Value, Array, Lock

import ctypes

def increment_counter(counter, lock, iterations):

for _ in range(iterations):

with lock:

counter.value += 1

if __name__ == "__main__":

counter = Value(ctypes.c_int, 0)

lock = Lock()

processes = [

Process(target=increment_counter, args=(counter, lock, 1000))

for _ in range(4)

]

for p in processes:

p.start()

for p in processes:

p.join()

print(f"Final counter value: {counter.value}") # Expected: 4000

“`

Kilit olmadan, okuma-değiştirme-yazma döngüsündeki yarış koşulları nedeniyle nihai değer öngörülemeyen şekilde 4000’den az olurdu. Paylaşılan değiştirilebilir durumu her zaman bir `Lock` ile koruyun.

Karmaşık paylaşılan veri yapıları (listeler, sözlükler, özel nesneler) için, nesneleri yöneten ve proxy erişimi sağlayan bir sunucu süreci oluşturan `multiprocessing.Manager` kullanın. Bunun bedeli, ham paylaşılan belleğe kıyasla erişim başına daha yüksek gecikmedir.

Örnek 5: Doğrudan İki Süreç İletişimi için Pipe

`multiprocessing.Pipe` bir çift bağlantı nesnesi oluşturur. Tam olarak iki süreç arasındaki noktadan noktaya iletişim için `Queue`’dan daha hızlıdır; çünkü daha az ek yüke sahiptir.

“`python

from multiprocessing import Process, Pipe

def worker(conn):

data = conn.recv()

result = [x ** 3 for x in data]

conn.send(result)

conn.close()

if __name__ == "__main__":

parent_conn, child_conn = Pipe()

p = Process(target=worker, args=(child_conn,))

p.start()

parent_conn.send([1, 2, 3, 4, 5])

result = parent_conn.recv()

p.join()

print(f"Cubed values: {result}")

“`

Birden fazla üretici veya tüketici söz konusu olduğunda `Queue` kullanın. Tam olarak iki süreç doğrudan veri alışverişi yaptığında `Pipe` kullanın.

Örnek 6: `concurrent.futures.ProcessPoolExecutor` Kullanımı

Modern Python kodu (3.2+) için `concurrent.futures.ProcessPoolExecutor`, `multiprocessing.Pool` üzerinde daha üst düzey ve temiz bir API sağlar ve `Future` nesneleriyle doğal olarak entegre olur.

“`python

from concurrent.futures import ProcessPoolExecutor, as_completed

def heavy_computation(n):

return sum(i * i for i in range(n))

if __name__ == "__main__":

inputs = [106, 2 * 106, 3 * 106, 4 * 106]

with ProcessPoolExecutor(max_workers=4) as executor:

futures = {executor.submit(heavy_computation, n): n for n in inputs}

for future in as_completed(futures):

n = futures[future]

try:

result = future.result()

print(f"Input {n}: result = {result}")

except Exception as e:

print(f"Input {n} raised an exception: {e}")

“`

`as_completed()`, görev süreleri önemli ölçüde farklılık gösterdiğinde kullanışlı olan, gönderim sırasına göre değil tamamlanma sırasına göre future’ları verir.

Üretim Tuzakları ve Gelişmiş Değerlendirmeler

Daemon Süreçleri

`start()` çağrılmadan önce `process.daemon = True` ayarlamak, alt süreci bir daemon yapar. Daemon süreçleri, üst süreç çıktığında otomatik olarak sonlandırılır; bu da sahipsiz arka plan çalışanlarını önler. Ancak daemon süreçleri kendileri alt süreç oluşturamaz.

Çalışan Süreçlerde İstisna İşleme

Çalışan fonksiyonların içinde fırlatılan istisnalar, `Pool.map()` kullanılırken üst sürece otomatik olarak yayılmaz — döndürülen değer üzerinde `result()` çağırdığınızda veya `map()` döndüğünde yeniden fırlatılırlar. `apply_async` ile istisnaları ortaya çıkarmak için `AsyncResult` üzerinde açıkça `.get()` çağırmanız gerekir.

“`python

from multiprocessing import Pool

def risky_function(x):

if x == 3:

raise ValueError(f"Cannot process value {x}")

return x * 10

if __name__ == "__main__":

with Pool(2) as pool:

try:

results = pool.map(risky_function, [1, 2, 3, 4])

except ValueError as e:

print(f"Caught worker exception: {e}")

“`

Bellek Tüketimi

Oluşturulan her süreç, üst sürecin bellek ayak izini kopyalar (`fork`’da) veya tüm modülleri yeniden içe aktarır (`spawn`’da). 2 GB RAM tüketen bir üst süreç için, `fork` tabanlı bir sistemde 8 çalışan oluşturmak, copy-on-write devreye girmeden önce 16 GB tüketiyormuş gibi görünebilir. Çalışan sayısını ölçeklendirmeden önce bellek kullanımınızı dikkatlice profilleyin.

Global Durumdan Kaçınma

Üst süreçteki global değişkenler, `spawn`’dan sonra alt süreçlerle paylaşılmaz. Bir alt süreçte global değişkenlerde yapılan değişiklikler, üst süreç ve diğer alt süreçler tarafından görünmez. Global yapılandırmaya güveniyorsanız, bunu açıkça argüman olarak iletin veya bir `Manager` kullanın.

Havuz Verimliliği için Parçalama

`pool.map()` bir `chunksize` parametresi kabul eder. Büyük yinelenebilirler için uygun bir parça boyutu ayarlamak, pickle/unpickle döngüsü başına birden fazla öğeyi toplu işleyerek IPC ek yükünü azaltır:

“`python

results = pool.map(process_item, large_list, chunksize=500)

“`

Multiprocessing İş Yükleri için Doğru Donanımı Seçme

Herhangi bir multiprocessing uygulamasının performans tavanı, nihayetinde mevcut fiziksel CPU çekirdeği sayısıyla belirlenir. 4 çekirdekli bir makinede 32 çalışanlı bir süreç havuzu, 4 çalışanlı bir havuzdan daha iyi performans göstermez — bağlam değiştirme ek yükü nedeniyle daha yavaş olur.

Veri ardışık düzenleri, bilimsel hesaplama, toplu ML çıkarımı gibi CPU yoğun Python uygulamalarının üretim dağıtımları için özel hesaplama kaynaklarına ihtiyaç duyarsınız. Yüksek çekirdek sayılı işlemcilere sahip Dedicated Servers, paylaşılan ortamlarda var olan kaynak çekişmesini ortadan kaldırarak her çalışan sürece fiziksel bir çekirdeğe rakipsiz erişim sağlar.

Geliştirme, hazırlık veya orta düzey iş yükleri için, uygun boyutta bir VPS Hosting örneği, çalışan sayılarını mevcut vCPU’lara göre ayarlayabileceğiniz uygun maliyetli bir ortam sağlar. Python uygulama ortamınızı yönetmek için bir kontrol paneline ihtiyaç duyuyorsanız, VPS with cPanel dağıtımı ve süreç izlemeyi basitleştirir.

Python multiprocessing’in PyTorch veya CuPy gibi CUDA tabanlı kütüphanelerle birleştirildiği GPU hızlandırmalı iş yükleri için, GPU Hosting, paralel CPU ön işleme ile GPU hesaplama ardışık düzenlerini birlikte çalıştırmak için gerekli donanımı sağlar.

Multiprocessing destekli API’leri HTTPS üzerinden sunan uygulamalar dağıtırken, sunucunuzu düzgün yapılandırılmış bir SSL Certificate ile eşleştirmek, üretim güvenliği için vazgeçilmez bir temeldir.

Pratik Karar Matrisi

İş yükünüz için doğru yaklaşımı belirlemek amacıyla aşağıdaki kontrol listesini kullanın:

Doğrudan `multiprocessing.Process` kullanın:

  • Az sayıda, sabit sayıda heterojen göreviniz olduğunda
  • Her görevin ayrı bir yaşam döngüsü varsa ve bireysel izleme gerektiriyorsa
  • Süreç özellikleri (daemon, ad, benzerlik) üzerinde ayrıntılı kontrole ihtiyaç duyduğunuzda

`multiprocessing.Pool` veya `ProcessPoolExecutor` kullanın:

  • Aynı fonksiyonu birçok veri öğesine uyguladığınızda
  • Otomatik çalışan yaşam döngüsü yönetimi istediğinizde
  • Minimum ortak kodla sonuç toplamaya ihtiyaç duyduğunuzda

`multiprocessing.Queue` kullanın:

  • Üretici-tüketici mimariniz olduğunda
  • Birden fazla üretici veya tüketici söz konusu olduğunda
  • `maxsize` aracılığıyla geri basınç kontrolüne ihtiyaç duyduğunuzda

`multiprocessing.Pipe` kullanın:

  • Tam olarak iki süreç doğrudan iletişim kurduğunda
  • Mesaj başına gecikme, esneklikten daha önemli olduğunda

`multiprocessing.Value` / `Array` kullanın:

  • Birçok çalışan arasında basit sayısal durumu paylaştığınızda
  • Erişim sıklığı yüksek olduğunda ve Manager proxy ek yükü kabul edilemez olduğunda

`multiprocessing.Manager` kullanın:

  • Karmaşık Python nesnelerini (listeler, sözlükler) paylaşmanız gerektiğinde
  • Tutarlılık, ham erişim hızından daha önemli olduğunda

Multiprocessing’den tamamen kaçının:

  • Darboğazınız G/Ç (ağ, disk) ise — `asyncio` veya `threading` kullanın
  • Görevler çok kısa ömürlüyse (< 1 ms) — süreç oluşturma ek yükü baskın olacaktır
  • Kod tabanınız pickle edilemeyen nesnelere yoğun biçimde bağımlıysa

SSS

S: Python multiprocessing betiklerinde neden `if __name__ == "__main__":` kullanmam gerekiyor?

Windows’ta ve `spawn` başlatma yöntemi kullanılırken Python, ana modülü her alt süreçte yeniden içe aktarır. `__main__` koruması olmadan alt süreç, kendi alt süreçlerini özyinelemeli olarak oluşturmaya çalışır ve sonsuz bir fork bombası oluşturur. Bu koruma Windows’ta zorunludur ve tüm platformlarda en iyi uygulama olarak kabul edilir.

S: `pool.map()` ile `pool.imap()` arasındaki fark nedir?

`pool.map()` tüm yinelenebiliri hemen tüketir, tüm öğeleri serileştirir, çalışanlara dağıtır ve tüm sonuçlar bir listeye toplanana kadar engeller. `pool.imap()` tembeldir — öğeleri artımlı olarak gönderir ve bir yineleyici döndürür; bu da çok büyük veri kümeleri için bellek açısından verimlidir. Girdi yinelenebiliri belleğe rahatça sığmadığında `imap` kullanın.

S: Python multiprocessing süreçleri bir veritabanı bağlantısını paylaşabilir mi?

Hayır. Veritabanı bağlantıları pickle edilemez ve süreçler arasında iletilemez. Her çalışan süreç kendi bağlantısını kurmalıdır. Üst süreçte değil, çalışan fonksiyonun içinde başlatılan bir bağlantı havuzu kütüphanesi (`pool_pre_ping=True` ile `SQLAlchemy` gibi) kullanın.

S: Bir multiprocessing havuzunda klavye kesmelerini (Ctrl+C) nasıl düzgün şekilde işlerim?

`pool.map()` çağrınızı bir `try/except KeyboardInterrupt` bloğuna sarın ve `except` cümlesinde `pool.terminate()` ardından `pool.join()` çağırın. Ayrıca, üst süreç öldürüldüğünde otomatik olarak sonlandırılmalarını istiyorsanız çalışan süreçleri daemon süreçleri olarak ayarlayın. Açık işleme olmadan, çalışan süreçler üst süreç kesintiye uğradıktan sonra sahipsiz olarak çalışmaya devam edebilir.

S: Python multiprocessing, macOS’ta `fork` ile kullanmak güvenli midir?

Python 3.8’den itibaren macOS’taki varsayılan başlatma yöntemi `fork`’dan `spawn`’a değiştirildi; çünkü macOS’un Objective-C çalışma zamanı ve belirli C uzantılarıyla (NumPy ve PyTorch tarafından kullanılanlar dahil) birleşen `fork` kilitlenmelere neden oluyordu. macOS’ta her zaman `spawn` veya `forkserver` kullanın ve işletim sistemleri arasında farklılık gösteren varsayılanlara güvenmek yerine başlatma yöntemini açıkça belirleyin.

15%

Tüm Hosting Hizmetlerinde %15 indirim

Becerilerini test et ve herhangi bir hosting planında İndirim kazan

Kodu kullanın:

Skills
Başlayın