Pernahkah kamu menjalankan program Python yang terasa lambat padahal komputermu punya banyak core CPU? Masalahnya, secara bawaan Python menjalankan kode secara sekuensial — satu per satu. Untungnya, Python menyediakan dua modul bawaan yang bisa membuat programmu berjalan paralel: threading dan multiprocessing. Di artikel ini kamu akan belajar perbedaan keduanya, memahami kenapa ada Global Interpreter Lock (GIL), dan langsung mempraktikkan cara mempercepat program Python-mu dengan contoh nyata.
Memahami Global Interpreter Lock (GIL)
Sebelum membahas threading dan multiprocessing, kamu wajib memahami konsep Global Interpreter Lock (GIL). GIL adalah mekanisme di CPython (implementasi Python standar) yang memastikan hanya satu thread yang bisa mengeksekusi bytecode Python dalam satu waktu. Artinya, sekalipun kamu membuat banyak thread, mereka tidak akan benar-benar berjalan bersamaan untuk kode Python murni — mereka hanya bergantian dengan sangat cepat.
Perlu diketahui: GIL hanya berlaku untuk kode Python murni (pure Python). Operasi I/O seperti membaca file, melakukan request jaringan, atau menunggu respons database akan melepaskan GIL, sehingga thread lain tetap bisa berjalan.
Threading vs Multiprocessing vs asyncio
Ketiga pendekatan ini sama-sama membuat program berjalan konkuren, tetapi cara kerjanya berbeda. Threading cocok untuk tugas yang banyak menunggu (I/O-bound) seperti mengunduh file atau memanggil API. Multiprocessing menjalankan proses yang benar-benar terpisah sehingga cocok untuk tugas berat komputasi (CPU-bound). Sementara asyncio memakai event loop untuk I/O concurrency dalam satu thread. Jika ingin mendalami model asyncio, baca panduan lengkap asyncio di Python.
Threading untuk Tugas I/O-Bound
Bayangkan kamu harus mengunduh 20 file dari internet. Jika dilakukan satu per satu, total waktunya adalah penjumlahan dari seluruh waktu unduhan. Dengan threading, kamu bisa mengunduh beberapa file sekaligus. Cara paling mudah adalah memakai ThreadPoolExecutor dari modul concurrent.futures.
import time
from concurrent.futures import ThreadPoolExecutor
def unduh(url):
print(f"Mengunduh {url} ...")
time.sleep(1) # simulasi menunggu respons jaringan
return f"{url} selesai"
urls = [f"https://contoh.com/gambar/{i}.jpg" for i in range(8)]
start = time.perf_counter()
# Tanpa threading (sekuensial)
for u in urls:
unduh(u)
print(f"Sekuensial: {time.perf_counter() - start:.2f} detik")
start = time.perf_counter()
# Dengan threading (4 worker)
with ThreadPoolExecutor(max_workers=4) as executor:
hasil = list(executor.map(unduh, urls))
print(f"Threading: {time.perf_counter() - start:.2f} detik")
Dengan max_workers=4, empat unduhan berjalan bersamaan sehingga total waktu berkurang hingga sekitar seperempatnya (dari sekitar 8 detik menjadi sekitar 2 detik). Modul concurrent.futures mengelola pembuatan dan pengaturan thread secara otomatis — kamu tinggal fokus pada logika fungsinya.
Tips: Aturan umum untuk max_workers pada tugas I/O-bound adalah antara 4 x jumlah_core hingga 10 x jumlah_core. Lakukan eksperimen untuk menemukan angka terbaik bagi kasusmu.
Multiprocessing untuk Tugas CPU-Bound
Bagaimana jika tugasmu berat di sisi komputasi, seperti menghitung kuadrat jutaan angka atau memproses gambar? Karena GIL, threading tidak akan mempercepat tugas CPU-bound — thread hanya bergantian dan malah menambah overhead. Solusinya adalah multiprocessing, yang menjalankan fungsi di proses terpisah sehingga setiap proses mendapat GIL-nya sendiri dan bisa memakai core CPU yang berbeda.
import time
from concurrent.futures import ProcessPoolExecutor
def hitung_kuadrat(n):
total = 0
for i in range(n):
total += i * i
return total
angka = [10_000_000] * 4 # 4 tugas berat
start = time.perf_counter()
hasil_sekuensial = [hitung_kuadrat(x) for x in angka]
print(f"Sekuensial: {time.perf_counter() - start:.2f} detik")
start = time.perf_counter()
with ProcessPoolExecutor(max_workers=4) as executor:
hasil_paralel = list(executor.map(hitung_kuadrat, angka))
print(f"Multiprocessing: {time.perf_counter() - start:.2f} detik")
Pada komputer dengan 4 core, versi multiprocessing bisa selesai sekitar 4 kali lebih cepat. Perhatikan juga bahwa banyak pustaka komputasi seperti NumPy sebenarnya sudah menjalankan operasi berat di dalam C dan melepaskan GIL, sehingga kamu tidak selalu perlu memakai multiprocessing secara manual.
Perhatian: Kode yang memakai multiprocessing harus berada di dalam blok if __name__ == "__main__":, terutama di Windows dan macOS. Tanpa itu, proses anak akan mengimpor ulang modul dan bisa menyebabkan infinite loop.
Berbagi Data Antar Proses dengan Queue
Karena setiap proses memiliki memori sendiri, kamu tidak bisa sekadar berbagi variabel global seperti pada threading. Untuk bertukar data antar proses, gunakan Queue — pola producer-consumer di mana satu proses menghasilkan data dan proses lain memprosesnya.
from multiprocessing import Process, Queue
def produsen(q):
for i in range(5):
q.put(f"data-{i}")
print(f"Produsen mengirim data-{i}")
q.put(None) # penanda tidak ada data lagi
def konsumen(q):
while True:
item = q.get()
if item is None:
break
print(f"Konsumen memproses {item}")
if __name__ == "__main__":
q = Queue()
p1 = Process(target=produsen, args=(q,))
p2 = Process(target=konsumen, args=(q,))
p1.start()
p2.start()
p1.join()
p2.join()
print("Selesai")
Kapan Menggunakan Apa?
Berikut panduan singkat untuk memilih pendekatan yang tepat:
I/O-bound (mengunduh file, memanggil API, membaca database) → gunakan threading atau asyncio.
CPU-bound (komputasi berat, enkripsi, pengolahan gambar) → gunakan multiprocessing.
Task terdistribusi di banyak mesin → gunakan task queue seperti Celery.
Ringkasan: untuk tugas I/O-bound pakai threading (atau asyncio), untuk tugas CPU-bound pakai multiprocessing, dan untuk task terdistribusi lintas mesin gunakan task queue seperti Celery.
Kesimpulan
Memahami threading dan multiprocessing adalah kunci untuk membuat program Python yang cepat dan efisien. Intinya: pakai ThreadPoolExecutor saat programmu banyak menunggu (I/O), dan ProcessPoolExecutor saat programmu sibuk menghitung (CPU). Keduanya tersedia bawaan di Python lewat concurrent.futures, jadi kamu bisa langsung mencobanya tanpa instalasi tambahan. Selamat bereksperimen dan sampai jumpa di tutorial berikutnya!