Modul multiprocessing menjalankan pekerjaan pada proses terpisah, yaitu unit yang masing-masing memiliki interpreter Python dan memorinya sendiri. Karena tidak berbagi memori, setiap proses memegang GIL (Global Interpreter Lock) sendiri, sehingga beberapa proses dapat memanfaatkan beberapa inti CPU secara nyata. Topik ini membahas perbedaan proses dengan thread, pembuatan proses menggunakan kelas Process, distribusi tugas melalui Pool, serta biaya komunikasi antarproses yang menyertainya.
Perbedaan proses dan thread#
Proses dibuat oleh sistem operasi dengan memori yang terpisah sepenuhnya dari proses lain, sedangkan thread berbagi memori di dalam satu proses. Perbedaan itu menentukan segala hal lainnya, mulai dari kecepatan pembuatan, cara berkomunikasi, sampai jenis beban yang cocok. Tabel berikut merangkum perbandingannya.
| Aspek | Thread | Proses |
|---|---|---|
| Memori | berbagi memori dengan thread lain | memiliki memorinya sendiri |
| GIL | berbagi satu GIL, beban CPU tidak paralel | satu GIL per proses, beban CPU paralel |
| Biaya pembuatan | kecil | lebih besar karena interpreter disiapkan ulang |
| Komunikasi | membaca variabel yang sama | melalui Queue, Pipe, atau nilai balik Pool |
| Cocok untuk | pekerjaan yang menunggu I/O | pekerjaan komputasi berat |
Contoh berikut membuktikan bahwa proses anak benar-benar berbeda dari proses induk dengan membandingkan nomor identitas prosesnya. Nilai True dihasilkan karena os.getpid() pada proses anak mengembalikan nomor yang berbeda dari induknya.
import multiprocessing
import os
def pid_kerja(antrean):
antrean.put(os.getpid())
if __name__ == "__main__":
antrean = multiprocessing.Queue()
p = multiprocessing.Process(target=pid_kerja, args=(antrean,))
p.start()
pid_induk = os.getpid()
p.join()
pid_anak = antrean.get()
print(pid_anak != pid_induk)TrueKelas Process digunakan sama seperti Thread, yaitu dengan start() dan join(), tetapi hasil pekerjaannya tidak dapat dikembalikan lewat nilai balik fungsi. Hasil dikirim melalui multiprocessing.Queue, sebuah antrean yang aman dipakai lintas proses. Proses induk mengambil isi antrean dengan get() setelah seluruh proses anak selesai.
Process dan Queue#
Contoh berikut membagi tiga perhitungan kuadrat ke tiga proses dan mengumpulkan hasilnya pada proses induk. Hasil diurutkan sebelum dicetak karena urutan pengambilan dari antrean mengikuti urutan penyelesaian, bukan urutan pengiriman.
import multiprocessing
def hitung_kuadrat(angka, antrean):
antrean.put(angka * angka)
if __name__ == "__main__":
antrean = multiprocessing.Queue()
proses = []
for n in (3, 5, 8):
p = multiprocessing.Process(target=hitung_kuadrat, args=(n, antrean))
proses.append(p)
p.start()
for p in proses:
p.join()
hasil = [antrean.get() for _ in proses]
print(sorted(hasil))[9, 25, 64]Pola kerjanya selalu sama, yaitu proses anak menaruh hasil ke antrean, proses induk menunggu dengan join(), lalu mengambil seluruh isi antrean. Pola ini aman karena tidak ada dua proses yang menulis ke memori yang sama, sehingga tidak membutuhkan lock sama sekali. Komunikasi sepenuhnya diserahkan kepada antrean yang mengurus sinkronisasi di balik layar.
Pool untuk pembagian tugas#
Membuat satu proses untuk setiap tugas tidak efisien ketika tugasnya banyak. Kelas multiprocessing.Pool menyiapkan sejumlah proses pekerja sekali saja, lalu mendistribusikan tugas-tugas berikutnya ke pekerja yang sedang senggang. Metode map menjaga urutan hasil sesuai urutan daftar masukannya, sehingga hasilnya dapat digunakan tanpa pengurutan ulang.
import multiprocessing
def jumlah_besar(n):
return sum(range(n))
if __name__ == "__main__":
with multiprocessing.Pool(processes=4) as pool:
hasil = pool.map(jumlah_besar, [1_000_000, 2_000_000, 3_000_000, 4_000_000])
print(hasil)[499999500000, 1999999000000, 4499998500000, 7999998000000]Empat tugas penjumlahan tersebut dijalankan empat proses secara paralel, sehingga total waktunya mendekati setengah dari versi berurutan bila mesin memiliki setidaknya empat inti CPU. Durasi persisnya tidak dicetak karena bergantung pada mesin yang menjalankannya. Bentuk with memastikan pool ditutup dan seluruh pekerja berhenti setelah bloknya selesai.
Biaya komunikasi antarproses#
Setiap nilai yang dikirim antar proses melewati serialisasi yang disebut pickling, yaitu pengubahan objek menjadi rangkaian byte agar dapat dipindahkan, lalu dibongkar kembali di sisi penerima. Akibatnya setiap nilai yang melewati batas proses tersalin utuh, dan makin besar datanya, makin mahal biaya pengirimannya. Contoh berikut memperlihatkan akibat langsungnya, yaitu perubahan pada proses anak tidak pernah terlihat oleh proses induk.
import multiprocessing
def ubah_daftar(daftar, antrean):
daftar.append(999)
antrean.put(len(daftar))
if __name__ == "__main__":
data = [1, 2, 3]
antrean = multiprocessing.Queue()
p = multiprocessing.Process(target=ubah_daftar, args=(data, antrean))
p.start()
p.join()
print(antrean.get())
print(data)4
[1, 2, 3]Di dalam proses anak, panjang daftar menjadi 4 karena 999 berhasil ditambahkan. Pada proses induk, data tetap berisi tiga elemen karena proses anak bekerja pada salinannya sendiri. Karena itu tugas yang dikirim ke proses sebaiknya mandiri, menerima seluruh masukannya sebagai argumen, dan mengembalikan hasil ringkas alih-alih mengandalkan memori bersama.
Kesalahan umum#
Kesalahan pertama adalah membuat proses tanpa pengaman if __name__ == "__main__":. Pada Linux, metode mulai bawaan berupa fork, yaitu penyalinan langsung dari proses induk, sehingga contoh berikut masih berjalan. Windows dan macOS menggunakan spawn yang mengimpor ulang berkas utama pada setiap proses anak.
# tidak-dijalankan
import multiprocessing
def kerja():
print("tugas dijalankan")
p = multiprocessing.Process(target=kerja)
p.start()
p.join()Pada metode spawn, baris pembuatan proses dieksekusi ulang oleh setiap proses anak, sehingga anak mencoba membuat proses lagi tanpa henti dan berakhir dengan error RuntimeError pada fase bootstrapping. Perbaikannya adalah menaruh seluruh pembuatan Process dan Pool di dalam blok if __name__ == "__main__":, sebagaimana semua contoh di topik ini. Kebiasaan tersebut membuat program yang sama berjalan benar pada ketiga sistem operasi.
Kesalahan kedua adalah menyimpulkan kecepatan dari satu kali pengukuran. Waktu pembuatan proses dan distribusi tugasnya bervariasi antar eksekusi, sehingga perbandingan yang sah membutuhkan pengukuran berulang, misalnya dengan timeit, sebelum versi paralel dinyatakan lebih cepat. Pengukuran tunggal yang menunjukkan versi paralel lebih lambat sering kali hanya menangkap biaya startup yang belum diimbangi manfaat paralelismenya.
Ringkasan#
- Proses memiliki interpreter dan memori sendiri sehingga beban CPU berjalan paralel melewati GIL, dengan biaya pembuatan dan komunikasi yang lebih tinggi daripada thread.
multiprocessing.Processmenjalankan fungsi pada proses baru, danmultiprocessing.Queuemenjadi saluran hasil menuju proses induk.multiprocessing.Pool.mapmendistribusikan tugas ke kumpulan pekerja dan menjaga urutan hasil sesuai urutan daftar masukannya.- Setiap nilai yang melewati batas proses disalin melalui pickling, sehingga mutasi pada proses anak tidak pernah terlihat oleh proses induk.
Referensi#
- multiprocessing — Process-based parallelism — Python Software Foundation — Dokumentasi resmi modul multiprocessing beserta Process, Queue, dan Pool.
- multiprocessing.pool.Pool — Python Software Foundation — Referensi kelas Pool dan metode map untuk distribusi tugas.
- Programming guidelines — Python Software Foundation — Aturan pemakaian multiprocessing, termasuk kebutuhan pengaman main pada metode spawn.