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:

python
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:

text
Thread utama: MainThread
Memvalidasi data dimulai
Mengirim email dimulai
Memvalidasi data selesai
Mengirim email selesai
Program utama selesai

Mari kita telaah apa yang terjadi pada baris-baris kode di atas:

  1. Baris 9 (threading.main_thread().name): function threading.main_thread() mengembalikan object thread yang menjalankan program kita sejak awal. Thread tersebut bernama MainThread.
  2. Baris 11–12 (threading.Thread(target=..., args=...)): kedua baris hanya membuat object Thread; function kerjakan_tugas belum dieksekusi. Argument args wajib berupa tuple, makanya ada koma di ("Memvalidasi data", 0.2) meski isinya dua nilai.
  3. Baris 13–14 (t1.start()): method start() melahirkan thread baru di sistem operasi, lalu thread tersebut mengeksekusi kerjakan_tugas. Kedua pekerjaan kini berjalan tumpang tindih.
  4. Baris 15–16 (t1.join()): thread utama menunggu masing-masing thread selesai. Baris print("Program utama selesai") baru berjalan setelah kedua join() 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:

python
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:

text
Potongan   : Rp50000
Harga akhir: Rp200000

Object 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:

python
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:

text
[A] pesan ke-1
[B] pesan ke-1
[A] pesan ke-2
[B] pesan ke-2
[A] pesan ke-3
[B] pesan ke-3
Selesai

Baris [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:

python
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:

text
Log ke-1 ditulis
Log ke-2 ditulis
Log ke-3 ditulis
Program utama selesai - daemon dimatikan

Function 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.