Thread adalah unit eksekusi termurah yang dapat dijalankan sistem operasi di dalam satu process. Semua thread dalam satu process Python berbagi memori yang sama, sehingga bertukar data antar thread semudah membaca dictionary bersama. Pada Chapter ini kita akan belajar membuat thread dengan class threading.Thread, menunggu pekerjaannya selesai dengan join(), mengambil hasil lewat object bersama, dan mengenal thread utama beserta daemon thread.
Membuat dan Menjalankan Thread #
Class threading.Thread menerima dua argument penting: target (function yang akan dijalankan) dan args (tuple berisi argument untuk function tersebut). Kita mulai dari dua pekerjaan sederhana:
import threading
import time
def kerjakan_tugas(nama_tugas, detik):
print(f"{nama_tugas} dimulai")
time.sleep(detik)
print(f"{nama_tugas} selesai")
print(f"Thread utama: {threading.main_thread().name}")
t1 = threading.Thread(target=kerjakan_tugas, args=("Memvalidasi data", 0.2))
t2 = threading.Thread(target=kerjakan_tugas, args=("Mengirim email", 0.3))
t1.start()
t2.start()
t1.join()
t2.join()
print("Program utama selesai")Output yang Dihasilkan:
Thread utama: MainThread
Memvalidasi data dimulai
Mengirim email dimulai
Memvalidasi data selesai
Mengirim email selesai
Program utama selesaiMari kita telaah apa yang terjadi pada baris-baris kode di atas:
- Baris 9 (
threading.main_thread().name): functionthreading.main_thread()mengembalikan object thread yang menjalankan program kita sejak awal. Thread tersebut bernamaMainThread. - Baris 11–12 (
threading.Thread(target=..., args=...)): kedua baris hanya membuat objectThread; functionkerjakan_tugasbelum dieksekusi. Argumentargswajib berupa tuple, makanya ada koma di("Memvalidasi data", 0.2)meski isinya dua nilai. - Baris 13–14 (
t1.start()): methodstart()melahirkan thread baru di sistem operasi, lalu thread tersebut mengeksekusikerjakan_tugas. Kedua pekerjaan kini berjalan tumpang tindih. - Baris 15–16 (
t1.join()): thread utama menunggu masing-masing thread selesai. Barisprint("Program utama selesai")baru berjalan setelah keduajoin()kembali — itulah mengapa pesan penutup selalu muncul paling akhir.
Urutan baris “dimulai” dan “selesai” pada output di atas stabil karena durasi kedua tugas sengaja dibedakan. Akan tetapi, penjadwalan thread adalah hak sistem operasi; kita tidak boleh menulis program yang mengandalkan urutan pasti antar-thread.
Berbagi Data Antar Thread #
Function target tidak bisa mengembalikan nilai lewat return — hasil return dari thread justru hilang. Pola yang lazim: berikan object bersama (misalnya dict atau list) sebagai argument, lalu thread mengisi object tersebut:
import threading
def hitung_diskon(harga, persen, hasil):
potongan = harga * persen / 100
hasil["potongan"] = potongan
hasil["harga_akhir"] = harga - potongan
hasil = {}
t = threading.Thread(target=hitung_diskon, args=(250000, 20, hasil))
t.start()
t.join()
print(f"Potongan : Rp{hasil['potongan']:.0f}")
print(f"Harga akhir: Rp{hasil['harga_akhir']:.0f}")Output yang Dihasilkan:
Potongan : Rp50000
Harga akhir: Rp200000Object hasil dirujuk oleh dua pihak sekaligus: thread utama yang membacanya, dan thread baru yang menulisnya. Karena t.join() dipanggil sebelum thread utama membaca, tidak ada dua thread yang menyentuh hasil pada saat bersamaan — pola tulis-dulu-baru-baca seperti ini aman tanpa penguncian. Ketika banyak thread menulis ke satu object pada saat bersamaan, muncullah masalah race condition yang akan kita bedah tuntas pada Chapter Locks dan Synchronization.
Untuk melihat betapa nyatanya tumpang tindih, mari jalankan dua thread yang mencetak pesan berulang:
import threading
import time
def cetak_pesan(nama):
for nomor in range(1, 4):
time.sleep(0.1)
print(f"[{nama}] pesan ke-{nomor}")
t1 = threading.Thread(target=cetak_pesan, args=("A",))
t2 = threading.Thread(target=cetak_pesan, args=("B",))
t1.start()
t2.start()
t1.join()
t2.join()
print("Selesai")Output yang Dihasilkan:
[A] pesan ke-1
[B] pesan ke-1
[A] pesan ke-2
[B] pesan ke-2
[A] pesan ke-3
[B] pesan ke-3
SelesaiBaris [A] dan [B] berselang-seling: keduanya benar-benar berjalan bersamaan, bukan berurutan. Urutan pasti antar-baris dapat berbeda di setiap eksekusi — kadang [B] lebih dulu — dan kode di atas tidak boleh bergantung pada urutan tersebut.
Daemon Thread #
Thread biasa akan menahan program agar tetap hidup sampai thread tersebut selesai. Sebaliknya, daemon thread adalah thread latar belakang yang langsung dimatikan begitu semua thread non-daemon berakhir:
import threading
import time
def catat_log():
for nomor in range(1, 6):
print(f"Log ke-{nomor} ditulis")
time.sleep(0.2)
t = threading.Thread(target=catat_log)
t.daemon = True
t.start()
time.sleep(0.5)
print("Program utama selesai - daemon dimatikan")Output yang Dihasilkan:
Log ke-1 ditulis
Log ke-2 ditulis
Log ke-3 ditulis
Program utama selesai - daemon dimatikanFunction catat_log sebenarnya berisi lima log, tetapi thread utama hanya bertahan sekitar 0,5 detik. Ketika thread utama selesai, interpreter Python mematikan daemon thread secara paksa — log ke-4 dan ke-5 tidak sempat ditulis. Inilah gunanya daemon: pekerjaan latar belakang seperti pencatatan metrik atau autosave yang boleh putus di tengah jalan.
Thread utama program kita sendiri adalah thread non-daemon pertama. Interpreter Python menunggu semua thread non-daemon selesai sebelum proses berakhir — kecuali kita memanggil sys.exit() dari thread utama, yang juga tetap menunggu thread non-daemon bergabung.