Kita telah melalui threading, locks, multiprocessing, pool, futures, antrian, dan GIL — dan setiap alat terasa masuk akal saat dibahas sendiri-sendiri. Kesulitan muncul di proyek nyata: program kita berisi campuran pemanggilan API, query database, dan perhitungan; alat mana yang harus dipakai? Chapter penutup ini merangkum seluruh section menjadi satu tabel keputusan, satu kebiasaan benchmark, dan satu pola gabungan.
Tabel Keputusan #
Titik berangkat terbaik selalu sama: program kita paling banyak menunggu atau paling banyak menghitung?
| Kondisi program | Pilihan utama | Alasannya |
|---|---|---|
I/O-bound, memakai library blocking (requests, driver DB, boto3) | ThreadPoolExecutor | GIL dilepas selama penantian; kode tidak perlu ditulis ulang |
| I/O-bound, ribuan koneksi bersamaan | asyncio | coroutine jauh lebih ringan daripada thread |
| CPU-bound murni Python (parsing, kompresi, simulasi) | ProcessPoolExecutor | tiap process punya interpreter dan GIL sendiri |
| Komputasi numerik / array | NumPy, pandas | operasi berat berjalan di C yang melepaskan GIL |
| Data harus dibagi dan diubah bersamaan | threading + lock | process memaksa penyalinan data antar memori |
Dua baris pertama perlu catatan: asyncio menuntut kode ditulis gaya async/await dari awal dan library yang dipakai harus mendukung async — itulah topik Section Asynchronous Programming. Bila library favorit kita blocking dan berbasis thread, ThreadPoolExecutor tetap pilihan paling jujur.
Ukur Sebelum Memutuskan #
Tabel di atas adalah hipotesis, bukan vonis. Mesin, versi Python, dan bentuk data membuat hasil nyata sering mengejutkan, jadi kebiasaan yang harus kita bawa pulang sederhana: ukur. Berikut kerangka benchmark kecil yang membandingkan pola berurutan, thread, dan process pada dua jenis beban:
import time
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
def beban_cpu(batas):
total = 0
for i in range(batas):
total += i * i
return total
def beban_io(detik):
time.sleep(detik)
return detik
if __name__ == "__main__":
UKURAN = 12_000_000
PEMBAGIAN = [UKURAN, UKURAN]
mulai = time.perf_counter()
beban_cpu(UKURAN)
beban_cpu(UKURAN)
print(f"CPU berurutan : {time.perf_counter() - mulai:.2f} detik")
mulai = time.perf_counter()
with ThreadPoolExecutor(max_workers=2) as executor:
hasil = sum(executor.map(beban_cpu, PEMBAGIAN))
print(f"CPU thread : {time.perf_counter() - mulai:.2f} detik")
mulai = time.perf_counter()
with ProcessPoolExecutor(max_workers=2) as executor:
hasil = sum(executor.map(beban_cpu, PEMBAGIAN))
print(f"CPU process : {time.perf_counter() - mulai:.2f} detik")
mulai = time.perf_counter()
beban_io(0.2)
beban_io(0.2)
print(f"I/O berurutan : {time.perf_counter() - mulai:.2f} detik")
mulai = time.perf_counter()
with ThreadPoolExecutor(max_workers=2) as executor:
hasil = sum(executor.map(beban_io, [0.2, 0.2]))
print(f"I/O thread : {time.perf_counter() - mulai:.2f} detik")Output yang Dihasilkan (waktu bergantung pada mesin):
CPU berurutan : 1.61 detik
CPU thread : 1.65 detik
CPU process : 1.09 detik
I/O berurutan : 0.40 detik
I/O thread : 0.20 detikHasil pada mesin kami mengonfirmasi seluruh teori section ini: pada beban CPU, thread justru sedikit lebih lambat daripada berurutan (efek GIL plus biaya pergantian thread), sementara process memberi kemenangan nyata; pada beban I/O, thread memotong waktu tunggu menjadi hampir setengah. Jalankan benchmark ini di mesin Anda dengan bentuk beban yang mendekati program asli — angka dari mesin orang lain, termasuk angka di tutorial ini, tidak pernah menggantikan pengukuran sendiri.
Pola Gabungan dan Opsi Ketiga #
Program nyata jarang murni satu jenis beban, dan kedua dunia bisa digabung: sebuah ProcessPoolExecutor yang menjalankan beberapa process, di mana setiap process memakai ThreadPoolExecutor kecil untuk bagian-bagiannya yang menunggu jaringan. Aturan penggabungannya: process di luar (karena jumlahnya dibatasi core), thread di dalam. Pola berlapis seperti ini juga yang dipakai ProcessPoolExecutor sendiri untuk mengelola pekerjanya.
Terakhir, jangan lupakan opsi ketiga yang menempati wilayah berbeda: asyncio. Untuk server yang melayani ribuan koneksi, atau program pengambil data yang murni menunggu, coroutine dengan event loop tunggal lebih ringan daripada sekawanan thread — materinya dibahas tuntas pada Section Asynchronous Programming setelah section ini.
Checklist Sebelum Memilih #
- Ukur dulu: berapa persen waktu program yang dihabiskan menunggu I/O versus menghitung? (
time.perf_counter()sudah cukup untuk mengukur.) - Tentukan titik panas: concurrency hanya berguna pada bagian yang benar-benar memakan waktu, bukan pada seluruh program.
- I/O-bound? Mulai dari
ThreadPoolExecutorbila library-nya blocking; pindah keasynciobila jumlah koneksi menjadi ratusan ke atas. - CPU-bound? Gunakan
ProcessPoolExecutor, pastikan function dan argument picklable serta pelindungif __name__ == "__main__":terpasang. - Ada data bersama antar thread? Lindungi dengan
Lock, atau lebih baik lagi: kirim lewatqueue.Queue. - Ukur lagi setelah memakai concurrency, dan bandingkan dengan versi berurutan yang sama.
- Simpan hasil benchmark di repo — angka historis itu referensi ketika mesin atau versi Python berganti.
Dengan checklist ini ditutuplah perjalanan kita memahami concurrency dan parallelism di Python: dari konsep tumpang tindih, kunci dan antrian, hingga keputusan arsitektur yang berpijak pada pengukuran.