threading dan multiprocessing — Thread, Process, dan GIL

Beda thread dan process, alasan CPU-bound tak paralel karena GIL, ThreadPoolExecutor vs multiprocessing.Pool, panggilan subprocess perintah eksternal dan alur pakainya divisualkan.

Toolkit concurrency dan parallelism Python — threading (thread), multiprocessing (process), dan subprocess (perintah eksternal) — disandingkan berdampingan. Sandbox Python berbasis browser tidak bisa membuat thread atau process sungguhan, sehingga artikel ini memakai diagram dan blok kode read-only untuk memantapkan konsepnya.

Kode di sini tidak benar-benar berjalan

Blok code di artikel ini adalah contoh yang ditujukan untuk environment Python sungguhan. Pemanggilan seperti threading.Thread.start() atau multiprocessing.Pool.map() tidak akan bekerja di sandbox browser karena runtime tidak bisa membuat OS thread atau OS process. Sebagai gantinya, ada quiz pemeriksaan konsep di akhir.

Di mana threading / multiprocessing / subprocess cocok dipakai
threadingI/O-boundFile / DB / APImultiprocessingCPU-boundImaging / numeriksubprocessPerintah eksternalgit / ffmpeg / shellasyncio (sebelumnya)Banyak I/O konkuren100 panggilan Web API
threading untuk pekerjaan I/O-bound, multiprocessing untuk komputasi berat CPU, subprocess untuk memanggil perintah di luar Python. Disandingkan dengan asyncio (dibahas di artikel sebelumnya untuk banyak I/O konkuren), keempat peran ini jadi terlihat jelas.

Process vs thread

Process adalah unit eksekusi yang diberikan OS — ia memiliki memory space sendiri, dan process tidak saling mengganggu. Thread adalah unit eksekusi ringan di dalam satu process, dan ia berbagi memori dengan thread sesamanya. Berbagi itu membuat operan data jadi cepat, tetapi kamu harus mewaspadai race condition (beberapa thread menulis ke variabel yang sama secara bersamaan dan merusak hasilnya).

Model mental sederhananya: "process = berat tapi independen, thread = ringan tapi berbagi".

Process, thread, dan coroutine bersarang seperti ini
Process (multiprocessing membuat ini)
  • Unit eksekusi independen dari sudut pandang OS
  • Memori independen · Tanpa batasan GIL
  • Biaya startup tinggi; parallelism sungguhan bekerja
Thread (threading membuat ini)
  • Unit yang benar-benar menjalankan kode di dalam process
  • Memori bersama · Tunduk pada GIL
  • Membuahkan hasil untuk pekerjaan I/O-bound
Coroutine (asyncio menjalankan ini)
  • Berjalan dengan beralih di dalam satu thread
  • Lebih ringan lagi; bagus untuk banyak I/O konkuren
multiprocessing memunculkan process paling luar, threading memunculkan thread di lapisan tengah, dan asyncio menjalankan coroutine paling dalam. Tiga lapisan berbeda — apa yang ingin kamu paralelkan menentukan pilihannya.
Perbedaan process vs thread
Process(multiprocessing)Memori independenBiaya startup tinggiTanpa interferensiParallelism sungguhanThread(threading)Memori bersamaStartup ringanAwas raceBatasan GIL
Processmemori independen, biaya startup tinggi sebagai gantinya tanpa interferensi. Threadmemori bersama, ringan, tetapi sinkronisasi (Lock dan teman-temannya) dibutuhkan. GIL Python membatasi parallelism thread untuk pekerjaan CPU-bound.

threading dan GIL — batasan thread Python

Python (CPython) memiliki mekanisme bernama GIL (Global Interpreter Lock) yang memberlakukan batasan: "hanya satu thread yang bisa mengeksekusi bytecode Python pada suatu waktu". Untuk pekerjaan berat CPU, menjalankannya di banyak thread secara konkuren secara efektif tidak lebih cepat dari satu thread.

GIL — hanya satu thread yang berjalan pada satu waktu
Thread AKomputasiGILDipegang AThread BMenungguThread AI/O wait→ melepas GILGILDiambil BThread BMulai komputasiserah terima
GIL adalah satu lock yang menjaga hak untuk mengeksekusi bytecode Python. Thread mengantre untuk mendapatkannya dan melepaskannya begitu mereka kena I/O wait, yang membuat thread lain bisa masuk selama jendela waktu itu.

Sebaliknya, selama pekerjaan I/O-bound (waktunya didominasi penantian respons eksternal — jaringan, file, DB) Python melepaskan GIL, sehingga threading memang mempercepat pekerjaan I/O-bound. Meskipun begitu, jika beban kerjanya I/O-bound, asyncio biasanya punya overhead lebih kecil dan lebih mudah ditulis, jadi utamakan asyncio untuk kode baru.

Bagaimana GIL memengaruhi thread
CPU-bound(komputasi)threadingMengantre di GIL→ Tanpa parallelismmultiprocessing dibutuhkanI/O-bound(API / DB / file)threadingGIL dilepas saat I/O→ Berjalan konkuren(asyncio lebih ringan lagi)
Pekerjaan CPU-bound tetap serial di bawah GIL berapa pun thread yang kamu munculkan. Pekerjaan I/O-bound melepaskan GIL selama menunggu, sehingga threading benar-benar membantu. Untuk parallelism CPU-bound sungguhan, gunakan multiprocessing.
# threading: API tingkat rendah
import threading

def worker(name):
    print(f"{name} started")
    # lakukan sesuatu
    print(f"{name} done")

t1 = threading.Thread(target=worker, args=("A",))
t2 = threading.Thread(target=worker, args=("B",))
t1.start()
t2.start()
t1.join()  # tunggu sampai selesai
t2.join()

# concurrent.futures: API tingkat tinggi (direkomendasikan)
from concurrent.futures import ThreadPoolExecutor

def fetch(url):
    # di kode nyata: requests.get(url) atau pekerjaan I/O-bound lainnya
    return f"fetched: {url}"

with ThreadPoolExecutor(max_workers=4) as executor:
    urls = ["a.com", "b.com", "c.com"]
    results = list(executor.map(fetch, urls))
    print(results)

Pakai ThreadPoolExecutor untuk kode baru

Memakai threading.Thread secara langsung membuat manajemen lifecycle berantakan. concurrent.futures.ThreadPoolExecutor aman dikelola dengan with dan memungkinkan kamu memproses seluruh list dengan executor.map(func, iterable) sebagai satu API. Batas thread (max_workers) juga praktis untuk mencegah membanjiri server dengan koneksi.

Kapan threading cocok

threading / ThreadPoolExecutor bersinar saat kamu ingin mengisi waktu tunggu dengan task lain:

- Memproses beberapa Web API / query DB / file I/O secara konkuren

- Memparalelkan library sinkron yang sudah ada (tanpa dukungan async)

- Membaca output dari beberapa process yang dimunculkan subprocess secara konkuren

- Menjalankan pekerjaan background tanpa memblokir main loop GUI

multiprocessing — parallelism sungguhan

multiprocessing adalah modul untuk memunculkan beberapa process Python dan menjalankannya secara paralel. Karena process bebas dari batasan GIL, kamu bisa menjalankan pekerjaan CPU-bound dalam parallelism sungguhan — pada CPU 4 core, image processing atau number crunching menjadi sekitar 4x lebih cepat.

multiprocessing.Pool — parallelism sungguhan di 4 core
Input[d1, d2, d3, d4]Process 1Core 1heavy(d1)Process 2Core 2heavy(d2)Process 3Core 3heavy(d3)Process 4Core 4heavy(d4)Hasil[r1, r2, r3, r4]
Empat process Python berjalan di empat core CPU yang terpisah, jadi tidak ada batasan GIL dan kamu mendapat eksekusi paralel sungguhan. Pekerjaan CPU-bound dipercepat sekitar 4x.
from multiprocessing import Pool

def heavy(n):
    return sum(i * i for i in range(n))   # pekerjaan CPU-bound

if __name__ == "__main__":   # bentuk wajib untuk multiprocessing
    with Pool(processes=4) as pool:
        results = pool.map(heavy, [10**6, 10**6, 10**6, 10**6])
        print("sum:", sum(results))

multiprocessing membutuhkan `if __name__ == '__main__':`

multiprocessing bekerja dengan menjalankan ulang skrip parent di setiap process anak, jadi memanggil Pool(...).map(...) di top level memicu rekursi tak terhingga dan meledak. Metode spawn di Windows / macOS sangat ketat soal ini — selalu letakkan kode utamamu di dalam blok if __name__ == "__main__":.

subprocess — perintah eksternal

subprocess adalah modul untuk memanggil perintah eksternal (perintah shell OS) dari Python — menjalankan git status, mengonversi video dengan ffmpeg, memanggil shell script, dan kasus pemakaian "menjalankan program yang bukan Python" lainnya. Namanya mirip multiprocessing, tetapi alat yang sama sekali berbeda.

subprocess.run — memanggil perintah eksternal dari Python
Pythonsubprocess.run([...])OSmemunculkan processPerintah eksternalgit / ffmpeg dll.Mengembalikan stdout /returncode ke Python
Python meminta OS untuk menjalankan perintahprocess OS terpisah menjalankan perintah eksternalnya → stdout dan return code dikembalikan dalam objek CompletedProcess. Berguna untuk menyerahkan pekerjaan yang tidak bisa dilakukan Python sendirian.
import subprocess

result = subprocess.run(
    ["git", "status", "--short"],
    capture_output=True,
    text=True,
    check=True,    # melempar CalledProcessError saat gagal
)
print(result.stdout)
print("return code:", result.returncode)

Alur keputusan — yang mana kamu pilih?

Memilih di antara asyncio / threading / multiprocessing / subprocess tergantung dua sumbu: "CPU-bound atau I/O-bound?" dan "Di dalam Python atau perintah eksternal?". Diagram alur di bawah menghilangkan sebagian besar tebak-tebakannya.

Alur keputusan concurrency / parallelism
Memanggilperintah eksternal?I/O-bound?(waktu tunggu mendominasi)CPU-bound?(komputasi mendominasi)→ subprocess→ asyncio(atau threading)→ multiprocessingYaYaYa
Perintah eksternal → subprocess, I/O-bound → asyncio (atau threading), CPU-bound → multiprocessing. Tiga sumbu untuk memilih alat yang tepat.
Beban kerjaPilihAlasannya
Memanggil 100 Web API secara konkurenasyncioI/O-bound; ringan dan mudah ditulis
Memparalelkan HTTP client sinkron yang sudah adathreading (ThreadPoolExecutor)Jika library tidak punya dukungan async, pakai threading
Memparalelkan image processing di 4 coremultiprocessingPekerjaan CPU-bound butuh process untuk menghindari GIL
Menjalankan perintah seperti git atau ffmpegsubprocessKhusus untuk memanggil program di luar Python
Jutaan operasi matematika sederhanaNumPy / CythonVektorisasi mengalahkan parallelism level Python

Benar-benar CPU-bound lebih jarang dari yang kamu kira

Banyak kode Python yang terlihat seperti macet di komputasi sebenarnya menjadi 100x lebih cepat dengan vektorisasi memakai NumPy / Pandas / Cython. Sebelum mengejar 4x dengan multiprocessing, periksa dulu apa yang harus kamu lakukan: NumPy untuk numerik, Pandas untuk data, regex yang dioptimalkan untuk string.

QUIZ

Cek Pemahaman

Jawab setiap pertanyaan satu per satu.

Soal 1Karena GIL Python (Global Interpreter Lock), menambah lebih banyak thread tidak memparalelkan pekerjaan jenis apa?

Soal 2Yang mana kamu pakai untuk memanggil perintah eksternal seperti git status dari Python?

Soal 3Yang paling cocok untuk benar-benar memparalelkan komputasi numerik berat di 4 core CPU?