15%

Hemat 15% di Semua Layanan Hosting

Uji kemampuanmu dan dapatkan Diskon pada paket hosting apa saja

Gunakan kode:

Skills
Memulai
09.10.2024

Python Multiprocessing: Panduan Teknis Lengkap untuk Eksekusi Paralel

Python's modul multiprocessing memungkinkan eksekusi paralel sejati dengan menjalankan proses-proses independen di tingkat OS, masing-masing dengan ruang memori dan interpreter Python sendiri — sepenuhnya melewati Global Interpreter Lock (GIL). Tidak seperti thread, yang berbagi satu status interpreter dan diserialisasi oleh GIL, proses-proses terpisah berjalan secara bersamaan di semua core CPU yang tersedia, menjadikan multiprocessing sebagai alat yang tepat untuk beban kerja CPU-bound seperti komputasi numerik, pemrosesan gambar, dan inferensi machine learning.

Panduan ini mencakup segalanya mulai dari arsitektur dasar model proses Python hingga pola-pola lanjutan termasuk shared memory, process pool, komunikasi antar-proses, dan jebakan tingkat produksi yang sebagian besar tutorial tidak bahas sama sekali.

Mengapa GIL Membuat Multithreading Tidak Cukup untuk Pekerjaan CPU-Bound

Global Interpreter Lock adalah mutex yang melindungi jumlah referensi objek internal CPython. Hanya satu thread yang dapat memegang GIL dan mengeksekusi bytecode Python pada satu waktu tertentu. Untuk tugas-tugas I/O-bound — permintaan jaringan, kueri database, pembacaan file — thread tetap berguna karena GIL dilepaskan selama syscall I/O yang memblokir. Namun, untuk komputasi murni, thread bersaing untuk mendapatkan GIL secara terus-menerus, tidak menghasilkan paralelisme nyata bahkan pada mesin 64-core.

Multiprocessing sepenuhnya menghindari hal ini. Setiap proses yang dijalankan adalah proses OS yang lengkap dan independen dengan interpreter CPython, heap, dan GIL-nya sendiri. Penjadwal sistem operasi mendistribusikan proses-proses ini ke seluruh core fisik, menghasilkan paralelisme sejati.

Dampak GIL: Contoh Konkret

Pertimbangkan sebuah fungsi yang melakukan 10 juta penambahan bilangan bulat. Menjalankannya dalam dua thread pada mesin dual-core akan memakan waktu wall-clock yang kira-kira sama dengan menjalankannya dalam satu thread — terkadang lebih lama karena overhead GIL contention. Menjalankannya dalam dua proses terpisah akan memangkas waktu wall-clock menjadi setengahnya.

Multiprocessing vs. Multithreading vs. Asyncio

Memahami kapan menggunakan setiap model konkurensi sama pentingnya dengan mengetahui cara menggunakannya.

Fitur`multiprocessing``threading``asyncio`
Jenis paralelismeSejati (proses OS)Semu (dibatasi GIL)Kooperatif (single-threaded)
Bypass GILYaTidakTidak
Model memoriTerpisah per prosesBersamaBersama
Kasus penggunaan terbaikTugas CPU-boundI/O-bound + library lamaI/O-bound, konkurensi tinggi
Overhead komunikasiTinggi (memerlukan IPC)Rendah (shared memory)Rendah (coroutine)
Isolasi kesalahanKuat (isolasi crash)Lemah (crash satu thread dapat mematikan semua)Lemah
Overhead startupTinggiRendahSangat rendah
Penggunaan memori tipikalTinggiRendahSangat rendah

Aturan umum: Gunakan `multiprocessing` untuk pekerjaan CPU-bound, `threading` atau `asyncio` untuk pekerjaan I/O-bound. Jika Anda membutuhkan keduanya, `concurrent.futures` menyediakan antarmuka terpadu untuk kedua model tersebut.

Arsitektur Inti: Cara Python Menjalankan Proses

Python mendukung tiga metode start untuk membuat proses anak, dan pilihannya memiliki konsekuensi yang signifikan:

  • `fork` (default di Linux/macOS): Menyalin memori proses induk menggunakan copy-on-write. Cepat, tetapi dapat menyebabkan masalah dengan proses induk yang multithreaded atau ekstensi C yang memegang lock.
  • `spawn` (default di Windows, tersedia di semua platform): Memulai interpreter Python baru dan mengimpor modul. Lebih lambat tetapi lebih aman. Mengharuskan semua kode dapat diimpor, itulah mengapa guard `if __name__ == "__main__":` wajib digunakan.
  • `forkserver`: Proses server khusus yang melakukan fork sesuai permintaan. Menghindari masalah keamanan fork sambil lebih efisien daripada spawn murni untuk banyak proses berumur pendek.

Tetapkan metode start secara eksplisit di bagian atas titik masuk Anda:

“`python

import multiprocessing

if __name__ == "__main__":

multiprocessing.set_start_method("spawn")

“`

Kegagalan memahami metode start adalah salah satu sumber paling umum dari bug halus yang bergantung pada platform dalam kode multiprocessing produksi.

Mengimpor Modul

“`python

import multiprocessing

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

“`

Primitif Utama dan Perannya

PrimitifTujuan
`Process`Menjalankan satu proses independen
`Pool`Mengelola pool worker yang dapat digunakan kembali
`Queue`FIFO yang aman untuk thread dan proses untuk IPC
`Pipe`Koneksi dua endpoint yang cepat antara dua proses
`Lock` / `RLock`Mutual exclusion untuk sumber daya bersama
`Value` / `Array`Shared memory untuk tipe sederhana
`Manager`Objek proxy untuk status bersama yang kompleks
`Event` / `Semaphore`Primitif sinkronisasi

Contoh 1: Menjalankan Satu Proses

Kelas `Process` adalah blok bangunan fundamental. Kelas ini memetakan langsung ke proses OS.

“`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}")

“`

Atribut dan metode utama:

  • `target`: Callable yang akan dieksekusi dalam proses anak.
  • `args` / `kwargs`: Argumen yang diteruskan ke fungsi target.
  • `start()`: Melakukan fork atau spawn proses anak.
  • `join(timeout=None)`: Memblokir pemanggil hingga proses berakhir. Selalu panggil `join()` untuk mencegah proses zombie.
  • `exitcode`: `0` saat keluar dengan bersih, nilai negatif jika dimatikan oleh sinyal, nilai positif jika proses memunculkan exception yang tidak ditangani.
  • `is_alive()`: Mengembalikan `True` jika proses masih berjalan.
  • `terminate()` / `kill()`: Mengirim `SIGTERM` / `SIGKILL` masing-masing. Gunakan dengan hati-hati — sumber daya mungkin tidak dibersihkan.

Jebakan kritis: Jika Anda menjalankan proses tanpa memanggil `join()`, proses anak menjadi proses zombie pada sistem Unix, mengonsumsi entri tabel proses hingga proses induk keluar.

Contoh 2: Process Pool dengan `multiprocessing.Pool`

Untuk beban kerja yang menerapkan fungsi yang sama ke banyak item data, `Pool` jauh lebih efisien daripada mengelola instance `Process` individual secara manual. Ini mempertahankan sejumlah proses worker tetap dan mendistribusikan pekerjaan di antara mereka.

“`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}")

“`

Perbandingan Metode Pool

MetodeMemblokirMengembalikanTerbaik Untuk
`pool.map(f, iterable)`YaDaftar hasilMap paralel sederhana
`pool.imap(f, iterable)`LazyIteratorIterable besar, efisiensi memori
`pool.imap_unordered(f, iterable)`LazyIterator (tidak berurutan)Ketika urutan tidak penting
`pool.starmap(f, iterable)`YaDaftar hasilFungsi dengan banyak argumen
`pool.apply_async(f, args)`Tidak`AsyncResult`Fire-and-forget atau callback
`pool.map_async(f, iterable)`Tidak`AsyncResult`Pengiriman batch non-blocking

Jebakan — pemilihan ukuran pool: Menetapkan `processes` lebih tinggi dari `os.cpu_count()` jarang meningkatkan throughput untuk tugas CPU-bound dan meningkatkan overhead context-switching. Heuristik umum adalah `processes = os.cpu_count() – 1` untuk menyisakan satu core untuk OS dan proses utama.

Jebakan — serialisasi: Semua argumen dan nilai kembalian yang diteruskan antara proses utama dan worker diserialisasi menggunakan `pickle`. Objek yang tidak dapat di-pickle (fungsi lambda, fungsi bersarang yang didefinisikan di dalam fungsi lain, handle file, koneksi database) akan memunculkan `PicklingError`. Gunakan `pool.starmap` dengan fungsi tingkat modul, atau restrukturisasi kode Anda untuk menghindari penerusan objek yang tidak dapat di-pickle.

Contoh 3: Komunikasi Antar-Proses dengan Queue

`multiprocessing.Queue` adalah FIFO yang aman untuk proses yang dibangun di atas pipe dan lock. Ini adalah mekanisme standar untuk pola producer-consumer.

“`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()

“`

Catatan desain kritis: Jangan pernah menggunakan `queue.empty()` untuk menentukan kapan harus berhenti mengonsumsi. Pemeriksaan `empty()` tidak dapat diandalkan dalam konteks multiprocessing — kondisi race ada antara pemeriksaan dan `get()` berikutnya. Selalu gunakan nilai sentinel (seperti `None` atau objek `STOP` khusus) untuk memberi sinyal bahwa produksi telah selesai.

Contoh 4: Shared Memory dengan Value dan Array

Ketika proses perlu berbagi status numerik sederhana tanpa overhead `Queue`, `multiprocessing.Value` dan `multiprocessing.Array` menyediakan shared memory langsung yang didukung oleh `ctypes`.

“`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

“`

Tanpa lock, nilai akhir akan kurang dari 4000 secara tidak terduga karena race condition pada siklus baca-modifikasi-tulis. Selalu lindungi status yang dapat diubah bersama dengan `Lock`.

Untuk struktur data bersama yang kompleks (list, dict, objek kustom), gunakan `multiprocessing.Manager`, yang membuat proses server yang mengelola objek dan menyediakan akses proxy. Trade-off-nya adalah latensi yang lebih tinggi per akses dibandingkan dengan shared memory mentah.

Contoh 5: Pipe untuk Komunikasi Langsung Dua Proses

`multiprocessing.Pipe` membuat sepasang objek koneksi. Ini lebih cepat dari `Queue` untuk komunikasi point-to-point antara tepat dua proses karena memiliki overhead yang lebih sedikit.

“`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}")

“`

Gunakan `Queue` ketika beberapa producer atau consumer terlibat. Gunakan `Pipe` ketika tepat dua proses bertukar data secara langsung.

Contoh 6: Menggunakan `concurrent.futures.ProcessPoolExecutor`

Untuk kode Python modern (3.2+), `concurrent.futures.ProcessPoolExecutor` menyediakan API tingkat lebih tinggi yang lebih bersih dibandingkan `multiprocessing.Pool` dan terintegrasi secara alami dengan objek `Future`.

“`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()` menghasilkan future saat selesai, bukan dalam urutan pengiriman, yang berguna ketika durasi tugas bervariasi secara signifikan.

Jebakan Produksi dan Pertimbangan Lanjutan

Proses Daemon

Menetapkan `process.daemon = True` sebelum memanggil `start()` menjadikan proses anak sebagai daemon. Proses daemon secara otomatis dihentikan ketika proses induk keluar, mencegah worker latar belakang yang terbengkalai. Namun, proses daemon tidak dapat menjalankan proses anak sendiri.

Penanganan Exception dalam Proses Worker

Exception yang dimunculkan di dalam fungsi worker tidak secara otomatis merambat ke proses induk saat menggunakan `Pool.map()` — exception tersebut dimunculkan kembali saat Anda memanggil `result()` pada nilai yang dikembalikan atau saat `map()` mengembalikan. Dengan `apply_async`, Anda harus secara eksplisit memanggil `.get()` pada `AsyncResult` untuk memunculkan exception.

“`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}")

“`

Konsumsi Memori

Setiap proses yang dijalankan menduplikasi jejak memori induk (pada `fork`) atau mengimpor ulang semua modul (pada `spawn`). Untuk proses induk yang mengonsumsi 2 GB RAM, menjalankan 8 worker pada sistem berbasis `fork` dapat tampak mengonsumsi 16 GB sebelum copy-on-write mulai bekerja. Profilkan penggunaan memori Anda dengan cermat sebelum meningkatkan jumlah worker.

Menghindari Status Global

Variabel global dalam proses induk tidak dibagikan dengan proses anak setelah `spawn`. Perubahan yang dilakukan pada variabel global dalam proses anak tidak terlihat oleh induk dan anak-anak lainnya. Jika Anda mengandalkan konfigurasi global, teruskan secara eksplisit sebagai argumen atau gunakan `Manager`.

Chunking untuk Efisiensi Pool

`pool.map()` menerima parameter `chunksize`. Untuk iterable besar, menetapkan ukuran chunk yang tepat mengurangi overhead IPC dengan mengelompokkan beberapa item per siklus pickle/unpickle:

“`python

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

“`

Memilih Hardware yang Tepat untuk Beban Kerja Multiprocessing

Batas kinerja dari setiap aplikasi multiprocessing pada akhirnya ditentukan oleh jumlah core CPU fisik yang tersedia. Pool proses dengan 32 worker pada mesin 4-core tidak akan mengungguli pool dengan 4 worker — bahkan akan lebih lambat karena overhead context-switching.

Untuk deployment produksi aplikasi Python yang intensif CPU — pipeline data, komputasi ilmiah, inferensi ML batch — Anda memerlukan sumber daya komputasi yang didedikasikan. Server Dedicated dengan prosesor bercore tinggi menghilangkan persaingan sumber daya yang melekat pada lingkungan bersama, memberikan setiap proses worker akses tak terbatas ke core fisik.

Untuk pengembangan, staging, atau beban kerja sedang, instance VPS Hosting yang berukuran tepat menyediakan lingkungan yang hemat biaya di mana Anda dapat menyesuaikan jumlah worker terhadap vCPU yang tersedia. Jika Anda memerlukan control panel untuk mengelola lingkungan aplikasi Python Anda, VPS dengan cPanel menyederhanakan deployment dan pemantauan proses.

Untuk beban kerja yang dipercepat GPU di mana multiprocessing Python dikombinasikan dengan library berbasis CUDA seperti PyTorch atau CuPy, GPU Hosting menyediakan hardware yang diperlukan untuk menjalankan preprocessing CPU paralel bersama pipeline komputasi GPU.

Saat men-deploy aplikasi yang mengekspos API berbasis multiprocessing melalui HTTPS, memasangkan server Anda dengan Sertifikat SSL yang dikonfigurasi dengan benar adalah dasar yang tidak dapat dinegosiasikan untuk keamanan produksi.

Matriks Keputusan Praktis

Gunakan daftar periksa berikut untuk menentukan pendekatan yang tepat untuk beban kerja Anda:

Gunakan `multiprocessing.Process` secara langsung ketika:

  • Anda memiliki sejumlah kecil tugas heterogen yang tetap
  • Setiap tugas memiliki siklus hidup yang berbeda dan memerlukan pemantauan individual
  • Anda memerlukan kontrol terperinci atas atribut proses (daemon, nama, afinitas)

Gunakan `multiprocessing.Pool` atau `ProcessPoolExecutor` ketika:

  • Anda menerapkan fungsi yang sama ke banyak item data
  • Anda menginginkan manajemen siklus hidup worker otomatis
  • Anda memerlukan pengumpulan hasil dengan boilerplate minimal

Gunakan `multiprocessing.Queue` ketika:

  • Anda memiliki arsitektur producer-consumer
  • Beberapa producer atau consumer terlibat
  • Anda memerlukan kontrol backpressure melalui `maxsize`

Gunakan `multiprocessing.Pipe` ketika:

  • Tepat dua proses berkomunikasi secara langsung
  • Latensi per pesan lebih penting daripada fleksibilitas

Gunakan `multiprocessing.Value` / `Array` ketika:

  • Anda berbagi status numerik sederhana antara banyak worker
  • Frekuensi akses tinggi dan overhead proxy Manager tidak dapat diterima

Gunakan `multiprocessing.Manager` ketika:

  • Anda perlu berbagi objek Python yang kompleks (list, dict)
  • Konsistensi lebih penting daripada kecepatan akses mentah

Hindari multiprocessing sepenuhnya ketika:

  • Bottleneck Anda adalah I/O (jaringan, disk) — gunakan `asyncio` atau `threading`
  • Tugas-tugas berumur sangat pendek (< 1 ms) — overhead spawn proses akan mendominasi
  • Codebase Anda sangat bergantung pada objek yang tidak dapat di-pickle

FAQ

T: Mengapa saya harus menggunakan `if __name__ == "__main__":` dalam skrip multiprocessing Python?

Di Windows dan saat menggunakan metode start `spawn`, Python mengimpor ulang modul utama di setiap proses anak. Tanpa guard `__main__`, proses anak akan mencoba menjalankan anak-anaknya sendiri secara rekursif, menyebabkan fork bomb tak terbatas. Guard ini wajib di Windows dan merupakan praktik terbaik di semua platform.

T: Apa perbedaan antara `pool.map()` dan `pool.imap()`?

`pool.map()` mengonsumsi seluruh iterable sekaligus, menserialisasi semua item, mendistribusikannya ke worker, dan memblokir hingga semua hasil dikumpulkan ke dalam list. `pool.imap()` bersifat lazy — ia mengirimkan item secara bertahap dan mengembalikan iterator, menjadikannya efisien memori untuk dataset yang sangat besar. Gunakan `imap` ketika iterable input tidak muat dengan nyaman di memori.

T: Dapatkah proses multiprocessing Python berbagi koneksi database?

Tidak. Koneksi database tidak dapat di-pickle dan tidak dapat diteruskan antar proses. Setiap proses worker harus membuat koneksinya sendiri. Gunakan library connection pool (seperti `SQLAlchemy` dengan `pool_pre_ping=True`) yang diinisialisasi di dalam fungsi worker, bukan di proses induk.

T: Bagaimana cara menangani keyboard interrupt (Ctrl+C) dengan baik dalam pool multiprocessing?

Bungkus panggilan `pool.map()` Anda dalam blok `try/except KeyboardInterrupt` dan panggil `pool.terminate()` diikuti oleh `pool.join()` dalam klausa `except`. Selain itu, tetapkan proses worker sebagai proses daemon jika Anda ingin mereka dihentikan secara otomatis ketika induk dimatikan. Tanpa penanganan eksplisit, proses worker mungkin terus berjalan sebagai orphan setelah induk diinterupsi.

T: Apakah multiprocessing Python aman digunakan dengan `fork` di macOS?

Sejak Python 3.8, metode start default di macOS berubah dari `fork` menjadi `spawn` khususnya karena `fork` yang dikombinasikan dengan runtime Objective-C macOS dan ekstensi C tertentu (termasuk yang digunakan oleh NumPy dan PyTorch) menyebabkan deadlock. Selalu gunakan `spawn` atau `forkserver` di macOS dan tetapkan metode start secara eksplisit daripada mengandalkan default, yang berbeda di berbagai sistem operasi.

15%

Hemat 15% di Semua Layanan Hosting

Uji kemampuanmu dan dapatkan Diskon pada paket hosting apa saja

Gunakan kode:

Skills
Memulai