Untuk kerja CPU-bound, thread di Python tidak menolong — Global Interpreter Lock memastikan hanya satu thread yang mengeksekusi bytecode pada satu waktu. Jalan keluarnya adalah multiprocessing: module bawaan Python yang membuat process sistem operasi terpisah, masing-masing dengan interpreter dan memori sendiri, sehingga dua core sungguhan bisa menghitung bersamaan. Pada Chapter ini kita akan membuat process, membuktikan bahwa memorinya terpisah, lalu mengukur speedup nyata.

Membuat Process Baru #

Penggunaan dasarnya paralel dengan threading.Thread — kita siapkan function target beserta argumentnya:

python
import multiprocessing
import os

def tugas_pekerja(nama_tugas):
    print(f"{nama_tugas} berjalan di process PID {os.getpid()}")

if __name__ == "__main__":
    print(f"Process utama berjalan di PID {os.getpid()}")
    p = multiprocessing.Process(target=tugas_pekerja, args=("Tugas A",))
    p.start()
    p.join()
    print("Process anak selesai")

Output yang Dihasilkan (angka PID berbeda pada setiap eksekusi):

text
Process utama berjalan di PID 6334
Tugas A berjalan di process PID 6335
Process anak selesai

Nilai dari os.getpid() adalah nomor process yang diberikan sistem operasi. Dua angka yang berbeda membuktikan bahwa tugas_pekerja benar-benar dieksekusi di process lain, bukan thread di process yang sama. Sama seperti thread, p.start() menjalankan dan p.join() menunggu selesai.

Memori Setiap Process Terpisah #

Thread berbagi satu memori; process tidak. Setiap process anak menerima salinan memori process utama pada saat pembuatan, dan perubahan apa pun setelah itu bersifat lokal. Mari kita buktikan dengan counter global:

python
import multiprocessing

counter = 0

def tambah_seratus():
    global counter
    for _ in range(100):
        counter += 1
    print(f"Counter di process anak  : {counter}")

if __name__ == "__main__":
    p = multiprocessing.Process(target=tambah_seratus)
    p.start()
    p.join()
    print(f"Counter di process utama : {counter}")

Output yang Dihasilkan:

text
Counter di process anak  : 100
Counter di process utama : 0

Process anak berhasil menaikkan salinan counter miliknya hingga 100, tetapi setelah p.join() kembali, counter di process utama tetap 0. Pada threading, kedua pihak melihat variabel yang sama; pada multiprocessing, keduanya adalah dua variabel terpisah yang kebetulan bernama sama.

Konsekuensinya: hasil pekerjaan harus dikirim balik secara eksplisit, misalnya melalui multiprocessing.Queue (seperti demo berikutnya) atau multiprocessing.Value. Harga pemisahan ini sekaligus menjadi keunggulannya — tanpa memori bersama, data race sebagaimana di Chapter Locks dan Synchronization tidak mungkin terjadi antar process.

Real Speedup for CPU Work #

Now the main goal: measuring. We compare a large arithmetic workload run sequentially versus split into two processes. Each process puts its results into a multiprocessing.Queue to be summed by the main process:

python
import multiprocessing
import time

def jumlah_kuadrat(batas):
    total = 0
    for i in range(batas):
        total += i * i
    return total

def jumlah_kuadrat_antrian(batas, antrian):
    antrian.put(jumlah_kuadrat(batas))

if __name__ == "__main__":
    BATAS = 12_000_000

    mulai = time.perf_counter()
    hasil = jumlah_kuadrat(BATAS) + jumlah_kuadrat(BATAS)
    berurutan = time.perf_counter() - mulai
    print(f"Berurutan : {hasil} dalam {berurutan:.2f} detik")

    antrian = multiprocessing.Queue()
    mulai = time.perf_counter()
    p1 = multiprocessing.Process(target=jumlah_kuadrat_antrian, args=(BATAS, antrian))
    p2 = multiprocessing.Process(target=jumlah_kuadrat_antrian, args=(BATAS, antrian))
    p1.start()
    p2.start()
    p1.join()
    p2.join()
    hasil = antrian.get() + antrian.get()
    paralel = time.perf_counter() - mulai
    print(f"Paralel   : {hasil} dalam {paralel:.2f} detik")

Output yang Dihasilkan (waktu bergantung pada mesin):

text
Berurutan : 1151999856000004000000 dalam 1.58 detik
Paralel   : 1151999856000004000000 dalam 1.13 detik

Two things are worth noting. First, the results of both versions are identical — the parallelism does not change the math. Second, the total time is reduced because two cores compute simultaneously; the gap is not a full 2x because of process creation, sending results via the queue, and the scheduling of the operating system. On machines with more free cores and larger workloads, the savings will be more pronounced.

Manual creation of Process like this gives us full control, but for a dozen tasks it becomes tedious: we have to arrange the queue, start, and join them one by one. The next step is Process Pools, which package that pattern into a practical API.