Thread Pool Pattern #

Dalam sistem modern, concurrency adalah kunci untuk mencapai performa dan skalabilitas yang baik. Aplikasi server, worker background, message processor, hingga sistem distributed hampir selalu berhadapan dengan banyak pekerjaan (tasks) yang datang secara bersamaan.

Masalahnya: membuat thread atau goroutine baru untuk setiap task bukanlah solusi yang selalu efisien. Overhead pembuatan thread, context switching, konsumsi memori, dan kurangnya kontrol dapat dengan cepat menurunkan performa sistem.

Di sinilah Thread Pool Pattern berperan. Pattern ini adalah salah satu core concurrency pattern yang paling sering digunakan dalam dunia software engineering.

Apa itu Thread Pool Pattern? #

Thread Pool Pattern adalah pola concurrency di mana:

  • Sejumlah thread (atau worker) dibuat terlebih dahulu
  • Thread-thread tersebut hidup sepanjang lifecycle tertentu
  • Task dikirim ke sebuah queue
  • Worker akan mengambil task dari queue dan mengeksekusinya

Alih-alih membuat thread baru setiap kali ada pekerjaan, sistem mendaur ulang thread yang sudah ada.

Secara konseptual:

[Task Producer] -> [Task Queue] -> [Worker 1]
                                   [Worker 2]
                                   [Worker N]

Di Golang, walaupun istilahnya bukan thread melainkan goroutine, konsep thread pool tetap relevan dan sering disebut sebagai Worker Pool Pattern.


Tujuan Pattern #

Thread Pool Pattern bertujuan untuk:

  1. Membatasi Concurrency Mencegah sistem membuat terlalu banyak thread/goroutine yang bisa menyebabkan resource exhaustion.

  2. Mengurangi Overhead Pembuatan thread mahal. Pool menghindari create–destroy thread berulang.

  3. Kontrol Resource CPU, memory, file descriptor, dan connection bisa dikontrol lebih baik.

  4. Meningkatkan Stabilitas Sistem Sistem tetap responsif meskipun terjadi lonjakan request.

  5. Menyederhanakan Manajemen Task Task diproses dengan mekanisme terpusat dan terprediksi.


Kapan Cocok Digunakan? #

Thread Pool Pattern sangat cocok ketika:

  • Beban kerja bersifat repetitif dan homogen
  • Jumlah task lebih besar dari resource yang tersedia
  • Task bersifat I/O-bound atau CPU-bound yang bisa dikontrol
  • Aplikasi perlu throughput stabil, bukan latency ekstrem

Contoh Use Case Nyata #

  • HTTP request processing
  • Background job / worker service
  • Message consumer (Kafka, SQS, Pub/Sub)
  • Image / file processing
  • Email / notification sender
  • Rate-limited external API caller

Kurang Cocok Jika #

  • Task sangat sedikit
  • Task butuh latency ultra-rendah (overhead queue bisa terasa)
  • Task harus dieksekusi segera tanpa antrean

Contoh Implementasi (Golang) #

Golang tidak memiliki thread pool bawaan secara eksplisit, tetapi channel + goroutine adalah building block yang sempurna untuk membangunnya.

Contoh Implementasi Sederhana #

package main

import (
	fmt
	"sync"
)

// Task merepresentasikan pekerjaan
type Task func()

func worker(id int, jobs <-chan Task, wg *sync.WaitGroup) {
	defer wg.Done()

	for job := range jobs {
		fmt.Printf("Worker %d processing job\n", id)
		job()
	}
}

func main() {
	const workerCount = 3
	const jobCount = 10

	jobs := make(chan Task)
	var wg sync.WaitGroup

	// Start workers
	for i := 1; i <= workerCount; i++ {
		wg.Add(1)
		go worker(i, jobs, &wg)
	}

	// Send jobs
	for j := 1; j <= jobCount; j++ {
		jobID := j
		jobs <- func() {
			fmt.Printf("Executing job %d\n", jobID)
		}
	}

	close(jobs)
	wg.Wait()
}

Penjelasan Singkat #

  • jobs adalah task queue
  • Worker adalah goroutine yang looping membaca dari channel
  • Jumlah worker membatasi concurrency
  • Task dieksekusi secara paralel namun terkendali

Variasi dan Peningkatan Desain #

Bounded Queue #

Gunakan buffered channel untuk membatasi jumlah task yang bisa antre:

jobs := make(chan Task, 100)

Ini mencegah producer mengirim task tanpa batas.

Context untuk Graceful Shutdown #

Tambahkan context.Context agar worker bisa berhenti dengan rapi:

  • Saat aplikasi shutdown
  • Saat terjadi timeout
  • Saat menerima signal OS

Error Handling #

Thread pool sebaiknya:

  • Mengembalikan error ke caller
  • Atau mengirim error ke error channel
  • Atau mencatat error secara terpusat

Dynamic Pool Size #

Pool bisa dibuat:

  • Fixed size (paling umum)
  • Semi-dynamic (naik turun berdasarkan load)

Namun dynamic pool menambah kompleksitas dan jarang diperlukan.


Thread Pool vs Goroutine Tanpa Batas #

AspekTanpa PoolDengan Pool
Kontrol Resource
OverheadTinggiRendah
StabilitasRentanStabil
KompleksitasRendahSedikit lebih tinggi

Di Golang, goroutine is cheap, tapi bukan gratis. Thread Pool tetap relevan.


Hubungan dengan Pattern Lain #

Thread Pool sering dikombinasikan dengan:

  • Producer–Consumer Pattern
  • Queue-Based Load Leveling
  • Bulkhead Pattern (resilience)
  • Rate Limiter

Dalam sistem besar, thread pool adalah fondasi dari banyak pattern lain.


Best Practices #

  1. Sesuaikan Jumlah Worker

    • CPU-bound: ≈ jumlah core CPU
    • I/O-bound: bisa lebih besar, tapi tetap dibatasi
  2. Gunakan Backpressure Jangan biarkan task queue tidak terbatas.

  3. Jangan Block Terlalu Lama di Worker Worker yang block lama = throughput turun.

  4. Pisahkan Task Producer dan Consumer Ini membuat desain lebih modular dan testable.

  5. Perhatikan Memory Allocation Task closure besar bisa meningkatkan GC pressure.

  6. Instrumentasi & Monitoring Pantau:

    • Queue length
    • Task latency
    • Worker utilization

Penutup #

Thread Pool Pattern adalah salah satu concurrency pattern paling fundamental dan praktis. Ia membantu sistem tetap stabil, efisien, dan terkontrol di bawah beban tinggi.

Di Golang, meskipun goroutine sangat ringan, penggunaan worker pool tetap menjadi praktik terbaik untuk:

  • Melindungi resource
  • Mengontrol concurrency
  • Membangun sistem yang production-ready

Jika Anda membangun service, worker, atau backend apa pun yang memproses banyak task paralel — Thread Pool bukan opsi, tapi kebutuhan.

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact