Guarded Suspension Pattern #

Bayangkan seorang petugas gudang yang tugasnya mengambil paket dari rak dan memprosesnya. Jika rak kosong, ada dua pilihan: dia terus bolak-balik mengecek rak setiap detik (busy waiting), atau dia duduk dan menunggu bel berbunyi — hanya bangun ketika ada yang menaruh paket baru di rak. Pilihan pertama membuang energi sia-sia; pilihan kedua efisien dan tepat sasaran. Inilah intuisi di balik Guarded Suspension Pattern: sebuah goroutine yang tidak bisa melanjutkan pekerjaannya karena suatu kondisi belum terpenuhi — data belum ada, resource masih dipakai, event belum terjadi — menggantungkan dirinya (suspend) sampai kondisi itu berubah. Tidak ada polling. Tidak ada busy loop. CPU bebas mengerjakan hal lain selagi menunggu. Di Go, dua mekanisme utama untuk ini adalah sync.Cond untuk skenario kondisi yang kompleks, dan channel untuk kasus yang lebih umum dan idiomatik.

Apa itu Guarded Suspension? #

Guarded Suspension Pattern adalah pola concurrency di mana sebuah goroutine memeriksa guard condition sebelum melanjutkan eksekusi, dan jika kondisi belum terpenuhi, goroutine tersebut di-suspend sampai kondisi berubah menjadi benar.

flowchart TD
    A([Goroutine ingin melanjutkan]) --> B{Guard Condition\nterpenuhi?}
    B -- Ya --> C[Lanjutkan eksekusi]
    B -- Tidak --> D[Suspend / tunggu]
    D --> E([Goroutine lain mengubah kondisi])
    E --> F[Kirim sinyal wakeup]
    F --> B
    C --> G([Selesai])

Tiga elemen yang selalu hadir dalam Guarded Suspension:

ElemenPeran
Guard ConditionEkspresi boolean yang harus true sebelum eksekusi bisa dilanjutkan
SuspensionGoroutine tidur tanpa membuang CPU saat kondisi belum terpenuhi
Wakeup SignalMekanisme yang membangunkan goroutine saat kondisi berubah

Perbedaan kritis antara Guarded Suspension dan busy waiting:

// ANTI-PATTERN: busy waiting — CPU terpakai penuh hanya untuk menunggu
for len(queue) == 0 {
    // spin loop — membuang CPU cycle secara sia-sia
    time.Sleep(1 * time.Millisecond) // bahkan dengan sleep, ini tetap polling
}
process(queue[0])

// BENAR: guarded suspension — CPU bebas dipakai goroutine lain
mu.Lock()
for len(queue) == 0 {
    cond.Wait() // lepas mutex, suspend, tunggu sinyal — CPU tidak terbuang
}
item := queue[0]
mu.Unlock()
process(item)

Implementasi dengan sync.Cond #

sync.Cond adalah condition variable di Go — primitif tingkat rendah yang memungkinkan goroutine menunggu pada kondisi yang kompleks. Ini adalah implementasi paling eksplisit dari Guarded Suspension.

package main

import (
	"fmt"
	"sync"
	"time"
)

// BoundedQueue adalah antrian dengan kapasitas maksimum
// Producer menunggu jika penuh, consumer menunggu jika kosong
type BoundedQueue struct {
	items    []int
	capacity int
	mu       sync.Mutex
	notEmpty *sync.Cond // sinyal: ada item baru (untuk consumer)
	notFull  *sync.Cond // sinyal: ada slot kosong (untuk producer)
}

func NewBoundedQueue(capacity int) *BoundedQueue {
	q := &BoundedQueue{
		items:    make([]int, 0, capacity),
		capacity: capacity,
	}
	q.notEmpty = sync.NewCond(&q.mu)
	q.notFull = sync.NewCond(&q.mu)
	return q
}

// Enqueue menambahkan item — menunggu jika queue penuh
func (q *BoundedQueue) Enqueue(item int) {
	q.mu.Lock()
	defer q.mu.Unlock()

	// GUARDED SUSPENSION: tunggu sampai ada slot kosong
	// WAJIB menggunakan 'for', bukan 'if' — alasan dijelaskan di bawah
	for len(q.items) >= q.capacity {
		fmt.Printf("[Producer] Queue penuh (%d/%d), menunggu...\n", len(q.items), q.capacity)
		q.notFull.Wait() // lepas mutex, suspend, tunggu sinyal notFull
	}

	q.items = append(q.items, item)
	fmt.Printf("[Producer] Enqueue %d (size: %d/%d)\n", item, len(q.items), q.capacity)
	q.notEmpty.Signal() // bangunkan satu consumer yang menunggu
}

// Dequeue mengambil item — menunggu jika queue kosong
func (q *BoundedQueue) Dequeue() int {
	q.mu.Lock()
	defer q.mu.Unlock()

	// GUARDED SUSPENSION: tunggu sampai ada item
	for len(q.items) == 0 {
		fmt.Println("[Consumer] Queue kosong, menunggu...")
		q.notEmpty.Wait() // lepas mutex, suspend, tunggu sinyal notEmpty
	}

	item := q.items[0]
	q.items = q.items[1:]
	fmt.Printf("[Consumer] Dequeue %d (size: %d/%d)\n", item, len(q.items), q.capacity)
	q.notFull.Signal() // bangunkan satu producer yang menunggu
	return item
}

// Size mengembalikan jumlah item saat ini
func (q *BoundedQueue) Size() int {
	q.mu.Lock()
	defer q.mu.Unlock()
	return len(q.items)
}

func main() {
	queue := NewBoundedQueue(3) // queue dengan kapasitas 3
	var wg sync.WaitGroup

	// Producer: menghasilkan 6 item (lebih dari kapasitas)
	wg.Add(1)
	go func() {
		defer wg.Done()
		for i := 1; i <= 6; i++ {
			queue.Enqueue(i)
			time.Sleep(100 * time.Millisecond)
		}
	}()

	// Consumer: memproses lebih lambat dari producer
	wg.Add(1)
	go func() {
		defer wg.Done()
		for i := 0; i < 6; i++ {
			time.Sleep(300 * time.Millisecond) // consumer lebih lambat
			item := queue.Dequeue()
			_ = item
		}
	}()

	wg.Wait()
	fmt.Println("Semua item diproses")
}

Mengapa for, Bukan if, Sebelum Wait() #

Ini adalah salah satu aturan terpenting dalam penggunaan sync.Cond — dan juga salah satu yang paling sering dilanggar.

// ANTI-PATTERN: menggunakan 'if' sebelum Wait()
func (q *Queue) DequeueWrong() int {
	q.mu.Lock()
	defer q.mu.Unlock()

	if len(q.items) == 0 { // ✗ 'if' hanya cek sekali
		q.cond.Wait()
		// Setelah dibangunkan, langsung lanjut — tanpa cek ulang!
		// Masalah: goroutine bisa dibangunkan oleh spurious wakeup
		// atau oleh Broadcast() meski kondisi belum terpenuhi
	}

	return q.items[0] // ✗ bisa panic jika queue masih kosong!
}

// BENAR: menggunakan 'for' sebelum Wait()
func (q *Queue) DequeueCorrect() int {
	q.mu.Lock()
	defer q.mu.Unlock()

	for len(q.items) == 0 { // ✓ 'for' selalu cek ulang setelah bangun
		q.cond.Wait()
		// Setelah dibangunkan, loop kembali ke kondisi
		// Jika kondisi masih belum terpenuhi → Wait() lagi
	}

	return q.items[0] // ✓ dijamin queue tidak kosong di sini
}

Ada dua alasan mengapa for wajib digunakan:

Spurious wakeup — kondisi variable bisa membangunkan goroutine tanpa ada yang memanggil Signal() atau Broadcast(). Ini bukan bug di Go, melainkan perilaku yang diizinkan oleh sistem operasi di bawahnya. Dengan for, goroutine akan langsung balik tidur jika kondisi ternyata belum terpenuhi.

Broadcast dan banyak waiter — ketika Broadcast() dipanggil, semua goroutine yang menunggu dibangunkan sekaligus. Tapi mungkin hanya satu item yang tersedia. Dengan for, hanya goroutine yang pertama kali memeriksa kondisi dan menemukan true yang akan melanjutkan; goroutine lain kembali menunggu.

sequenceDiagram
    participant G1 as Goroutine 1
    participant G2 as Goroutine 2
    participant Cond as sync.Cond
    participant P as Producer

    G1->>Cond: Wait() — menunggu
    G2->>Cond: Wait() — menunggu
    P->>Cond: Signal() — bangunkan SATU
    Cond-->>G1: dibangunkan
    G1->>G1: for: cek kondisi → true → lanjut
    Note over G2: masih menunggu
    P->>Cond: Signal() — bangunkan SATU
    Cond-->>G2: dibangunkan
    G2->>G2: for: cek kondisi → true → lanjut

Signal vs Broadcast #

sync.Cond menyediakan dua cara untuk membangunkan goroutine yang sedang menunggu:

// Signal: bangunkan SATU goroutine yang menunggu
// Gunakan ketika hanya satu goroutine yang bisa memanfaatkan perubahan
q.cond.Signal()

// Broadcast: bangunkan SEMUA goroutine yang menunggu
// Gunakan ketika perubahan relevan untuk semua waiter
q.cond.Broadcast()

Panduan memilih yang tepat:

SituasiPilihan
Hanya satu item tersedia, banyak consumerSignal() — hanya satu yang perlu bangun
Kondisi berubah dan semua waiter perlu mengevaluasi ulangBroadcast()
Shutdown atau cancellation — semua harus berhentiBroadcast()
Resource tersedia lebih dari cukup untuk semua waiterBroadcast()
// Contoh penggunaan Broadcast untuk shutdown
type WorkerPool struct {
	mu       sync.Mutex
	cond     *sync.Cond
	jobs     []Job
	shutdown bool
}

func (p *WorkerPool) Shutdown() {
	p.mu.Lock()
	p.shutdown = true
	p.mu.Unlock()
	p.cond.Broadcast() // bangunkan SEMUA worker agar masing-masing bisa exit
}

func (p *WorkerPool) worker() {
	p.mu.Lock()
	defer p.mu.Unlock()

	for {
		// Guard condition: ada pekerjaan ATAU shutdown diminta
		for len(p.jobs) == 0 && !p.shutdown {
			p.cond.Wait()
		}

		if p.shutdown && len(p.jobs) == 0 {
			return // exit dengan bersih
		}

		job := p.jobs[0]
		p.jobs = p.jobs[1:]
		p.mu.Unlock()   // lepas lock saat memproses
		job.Execute()
		p.mu.Lock()     // ambil kembali lock untuk iterasi berikutnya
	}
}

Implementasi Idiomatik dengan Channel #

Di Go, channel sudah mengimplementasikan Guarded Suspension secara built-in: receive dari channel kosong akan otomatis memblokir, dan send ke channel penuh juga akan memblokir. Ini adalah cara paling idiomatik untuk Guarded Suspension di Go.

package main

import (
	"context"
	"fmt"
	"time"
)

// GuardedQueue menggunakan channel sebagai mekanisme guarded suspension
type GuardedQueue struct {
	ch chan int
}

func NewGuardedQueue(capacity int) *GuardedQueue {
	return &GuardedQueue{
		ch: make(chan int, capacity), // buffered = kapasitas antrian
	}
}

// Enqueue mengirim item — memblokir jika penuh (guarded suspension built-in)
func (q *GuardedQueue) Enqueue(ctx context.Context, item int) error {
	select {
	case q.ch <- item: // ✓ guarded suspension: tunggu jika channel penuh
		return nil
	case <-ctx.Done():
		return ctx.Err()
	}
}

// Dequeue mengambil item — memblokir jika kosong (guarded suspension built-in)
func (q *GuardedQueue) Dequeue(ctx context.Context) (int, error) {
	select {
	case item := <-q.ch: // ✓ guarded suspension: tunggu jika channel kosong
		return item, nil
	case <-ctx.Done():
		return 0, ctx.Err()
	}
}

// Close menutup queue — consumer yang menunggu akan menerima zero value
func (q *GuardedQueue) Close() {
	close(q.ch)
}

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	queue := NewGuardedQueue(3)
	done := make(chan struct{})

	// Producer
	go func() {
		for i := 1; i <= 8; i++ {
			if err := queue.Enqueue(ctx, i); err != nil {
				fmt.Println("[Producer] Context cancelled:", err)
				return
			}
			fmt.Printf("[Producer] Enqueued: %d\n", i)
			time.Sleep(150 * time.Millisecond)
		}
		queue.Close()
	}()

	// Consumer
	go func() {
		defer close(done)
		for {
			item, err := queue.Dequeue(ctx)
			if err != nil {
				fmt.Println("[Consumer] Context cancelled:", err)
				return
			}
			fmt.Printf("[Consumer] Processing: %d\n", item)
			time.Sleep(400 * time.Millisecond)
		}
	}()

	<-done
}

Alur guarded suspension dengan channel bisa divisualisasikan:

flowchart LR
    P([Producer]) -->|ch <- item| B{Channel\npenuh?}
    B -- Ya --> PW[Producer menunggu\nguarded suspension]
    B -- Tidak --> CH[(Buffered\nChannel)]
    CH -->|<-ch| C([Consumer])
    C --> CW{Channel\nkosong?}
    CW -- Ya --> CWait[Consumer menunggu\nguarded suspension]
    CWait -->|ada item baru| CH
    PW -->|ada slot kosong| CH

Connection Pool: Use Case Nyata #

Connection pool adalah contoh sempurna Guarded Suspension — goroutine yang membutuhkan koneksi harus menunggu jika semua koneksi sedang dipakai.

package main

import (
	"context"
	"errors"
	"fmt"
	"sync"
	"time"
)

// Connection merepresentasikan koneksi database
type Connection struct {
	id   int
	busy bool
}

func (c *Connection) Execute(query string) string {
	time.Sleep(200 * time.Millisecond) // simulasi query
	return fmt.Sprintf("result from conn-%d: %s", c.id, query)
}

// ConnectionPool adalah pool dengan guarded suspension
type ConnectionPool struct {
	connections []*Connection
	available   []*Connection
	mu          sync.Mutex
	cond        *sync.Cond
}

func NewConnectionPool(size int) *ConnectionPool {
	pool := &ConnectionPool{}
	pool.cond = sync.NewCond(&pool.mu)

	for i := 0; i < size; i++ {
		conn := &Connection{id: i + 1}
		pool.connections = append(pool.connections, conn)
		pool.available = append(pool.available, conn)
	}
	return pool
}

// Acquire mendapatkan koneksi — menunggu jika semua sedang dipakai
func (p *ConnectionPool) Acquire(ctx context.Context) (*Connection, error) {
	p.mu.Lock()
	defer p.mu.Unlock()

	// GUARDED SUSPENSION: tunggu sampai ada koneksi tersedia
	for len(p.available) == 0 {
		// Cek context di dalam loop — tidak infinite wait jika ctx cancel
		select {
		case <-ctx.Done():
			return nil, errors.New("timeout menunggu koneksi")
		default:
		}
		fmt.Println("[Pool] Semua koneksi sibuk, menunggu...")
		p.cond.Wait()
	}

	// Ambil koneksi pertama yang tersedia
	conn := p.available[0]
	p.available = p.available[1:]
	conn.busy = true
	fmt.Printf("[Pool] Koneksi %d diberikan (sisa: %d)\n", conn.id, len(p.available))
	return conn, nil
}

// Release mengembalikan koneksi ke pool dan membangunkan waiter
func (p *ConnectionPool) Release(conn *Connection) {
	p.mu.Lock()
	defer p.mu.Unlock()

	conn.busy = false
	p.available = append(p.available, conn)
	fmt.Printf("[Pool] Koneksi %d dikembalikan (tersedia: %d)\n", conn.id, len(p.available))
	p.cond.Signal() // bangunkan satu goroutine yang menunggu
}

func main() {
	pool := NewConnectionPool(2) // hanya 2 koneksi

	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
	defer cancel()

	var wg sync.WaitGroup
	queries := []string{"SELECT users", "SELECT orders", "SELECT products", "SELECT inventory"}

	for i, query := range queries {
		wg.Add(1)
		go func(id int, q string) {
			defer wg.Done()

			conn, err := pool.Acquire(ctx)
			if err != nil {
				fmt.Printf("[Worker %d] Gagal mendapat koneksi: %v\n", id, err)
				return
			}
			defer pool.Release(conn)

			result := conn.Execute(q)
			fmt.Printf("[Worker %d] %s\n", id, result)
		}(i+1, query)
	}

	wg.Wait()
}

Timeout pada Guarded Suspension #

Menunggu tanpa batas adalah resep untuk deadlock. Selalu sediakan mekanisme timeout.

package main

import (
	"context"
	"fmt"
	"sync"
	"time"
)

type TimedQueue struct {
	items []int
	mu    sync.Mutex
	cond  *sync.Cond
}

func NewTimedQueue() *TimedQueue {
	q := &TimedQueue{}
	q.cond = sync.NewCond(&q.mu)
	return q
}

func (q *TimedQueue) Enqueue(item int) {
	q.mu.Lock()
	defer q.mu.Unlock()
	q.items = append(q.items, item)
	q.cond.Signal()
}

// DequeueWithTimeout menunggu item dengan batas waktu menggunakan context
func (q *TimedQueue) DequeueWithContext(ctx context.Context) (int, error) {
	// Jalankan goroutine watcher untuk membatalkan Wait() saat context selesai
	done := make(chan struct{})
	go func() {
		select {
		case <-ctx.Done():
			q.cond.Broadcast() // bangunkan semua waiter agar bisa cek ctx.Done()
		case <-done:
		}
	}()
	defer close(done)

	q.mu.Lock()
	defer q.mu.Unlock()

	for len(q.items) == 0 {
		// Cek ctx setelah setiap wakeup
		select {
		case <-ctx.Done():
			return 0, ctx.Err()
		default:
		}
		q.cond.Wait()
	}

	item := q.items[0]
	q.items = q.items[1:]
	return item, nil
}

func main() {
	queue := NewTimedQueue()

	// Consumer dengan timeout 1 detik
	ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
	defer cancel()

	go func() {
		time.Sleep(2 * time.Second) // terlambat — lewat deadline
		queue.Enqueue(42)
	}()

	item, err := queue.DequeueWithContext(ctx)
	if err != nil {
		fmt.Println("Timeout:", err)
	} else {
		fmt.Println("Diterima:", item)
	}
}

sync.Cond vs Channel: Kapan Memilih Mana? #

flowchart TD
    Q{Kondisi menunggu\nberbentuk apa?} --> Q1{Menunggu\nitem/data?}
    Q1 -- Ya --> CH[Channel ✓\nLebih idiomatik di Go]
    Q1 -- Tidak --> Q2{Kondisi bergantung\npada state kompleks?}
    Q2 -- Ya --> SC[sync.Cond ✓\nFit untuk kondisi arbitrer]
    Q2 -- Tidak --> Q3{Perlu\nBroadcast?}
    Q3 -- Ya --> SC
    Q3 -- Tidak --> CH
AspekChannelsync.Cond
Idiomatik di GoYa — disarankan tim GoLebih jarang dipakai
KondisiImplisit (channel kosong/penuh)Eksplisit, bisa kondisi apapun
BroadcastTidak ada langsungBroadcast() tersedia
Cancellationselect dengan ctx.Done()Perlu goroutine watcher terpisah
Kapasitas bufferDikontrol saat make(chan, N)Perlu logika manual
Cocok untukProducer-consumer, pipelineConnection pool, kondisi kompleks

Anti-Pattern yang Harus Dihindari #

// ✗ Busy waiting — CPU terbuang sia-sia
for len(queue) == 0 {
    time.Sleep(1 * time.Millisecond) // polling = boros CPU
}

// ✓ Guarded suspension — CPU bebas saat menunggu
mu.Lock()
for len(queue) == 0 {
    cond.Wait() // tidur, tidak polling
}
mu.Unlock()

// ✗ Menggunakan 'if' sebelum Wait() — bisa panic jika spurious wakeup
func (q *Queue) BadDequeue() int {
    q.mu.Lock()
    defer q.mu.Unlock()
    if len(q.items) == 0 { // ✗ cek sekali saja
        q.cond.Wait()
    }
    return q.items[0] // ✗ bisa panic!
}

// ✓ Selalu gunakan 'for' sebelum Wait()
func (q *Queue) GoodDequeue() int {
    q.mu.Lock()
    defer q.mu.Unlock()
    for len(q.items) == 0 { // ✓ cek ulang setelah bangun
        q.cond.Wait()
    }
    return q.items[0] // ✓ aman
}

// ✗ Lupa memanggil Signal/Broadcast — waiter menunggu selamanya
func (q *Queue) BrokenEnqueue(item int) {
    q.mu.Lock()
    defer q.mu.Unlock()
    q.items = append(q.items, item)
    // ✗ tidak ada Signal() — consumer tidur selamanya
}

// ✓ Selalu Signal/Broadcast setelah kondisi berubah
func (q *Queue) CorrectEnqueue(item int) {
    q.mu.Lock()
    defer q.mu.Unlock()
    q.items = append(q.items, item)
    q.cond.Signal() // ✓ bangunkan consumer yang menunggu
}

// ✗ Wait() di luar mutex — panic: sync: unlock of unlocked mutex
func (q *Queue) PanicDequeue() int {
    for len(q.items) == 0 {
        q.cond.Wait() // ✗ harus di dalam mutex lock!
    }
    return q.items[0]
}

// ✓ Wait() harus selalu berada di dalam mutex lock
func (q *Queue) SafeDequeue() int {
    q.mu.Lock()
    defer q.mu.Unlock()
    for len(q.items) == 0 {
        q.cond.Wait() // ✓ di dalam lock — aman
    }
    return q.items[0]
}

Checklist Review Guarded Suspension #

DESAIN KONDISI:
  □ Guard condition didefinisikan dengan jelas dan terdokumentasi
  □ 'for' digunakan (bukan 'if') sebelum setiap Wait()
  □ Kondisi dievaluasi ulang setelah setiap wakeup
  □ Seluruh akses ke shared state dilindungi mutex yang sama

SINYAL WAKEUP:
  □ Signal() atau Broadcast() dipanggil setiap kali kondisi berubah
  □ Pilihan Signal vs Broadcast sesuai dengan jumlah waiter yang perlu dibangunkan
  □ Broadcast() digunakan untuk shutdown/cancellation agar semua waiter exit

LIFECYCLE DAN TIMEOUT:
  □ Setiap Wait() punya jalur keluar selain kondisi terpenuhi (timeout, ctx cancel)
  □ Context digunakan untuk membatasi waktu tunggu maksimum
  □ Goroutine watcher diluncurkan untuk mem-Broadcast saat ctx.Done()

PEMILIHAN PRIMITIF:
  □ Channel dipertimbangkan sebelum sync.Cond untuk use case standar
  □ sync.Cond dipilih hanya jika kondisi tidak bisa direpresentasikan sebagai channel
  □ Tidak ada busy waiting (polling loop) — selalu gunakan Wait() atau channel block

PENGUJIAN:
  □ Diuji dengan go test -race untuk mendeteksi data race
  □ Skenario timeout dan cancellation diuji secara eksplisit
  □ Deadlock diverifikasi tidak terjadi ketika Signal/Broadcast tidak dipanggil

Ringkasan #

  • Guarded Suspension menunggu kondisi terpenuhi tanpa membuang CPU — goroutine di-suspend sepenuhnya, bukan polling, sehingga resource tersedia untuk goroutine lain yang produktif.
  • Tiga elemen wajib — guard condition (apa yang ditunggu), suspension (goroutine tidur), dan wakeup signal (pemberitahuan bahwa kondisi berubah).
  • Selalu gunakan for, bukan if, sebelum Wait() — spurious wakeup dan Broadcast() bisa membangunkan goroutine meski kondisi belum terpenuhi; for memastikan kondisi dicek ulang setelah setiap wakeup.
  • Wait() secara atomik melepas mutex dan men-suspend goroutine — saat dibangunkan, mutex dikunci kembali secara otomatis sebelum eksekusi berlanjut; selalu pastikan Wait() dipanggil di dalam lock.
  • Selalu panggil Signal() atau Broadcast() setelah kondisi berubah — lupa memanggil sinyal menyebabkan waiter tidur selamanya (deadlock).
  • Pilih Signal() untuk satu waiter, Broadcast() untuk semuaSignal() lebih efisien saat hanya satu goroutine yang bisa memanfaatkan perubahan; Broadcast() untuk shutdown atau perubahan yang relevan ke semua waiter.
  • Channel adalah implementasi Guarded Suspension yang paling idiomatik di Go — receive dari channel kosong otomatis memblokir; gunakan sync.Cond hanya untuk kondisi yang tidak bisa direpresentasikan sebagai channel.
  • Selalu integrasikan context untuk timeout — menunggu tanpa batas adalah resep deadlock; gunakan goroutine watcher yang memanggil Broadcast() saat ctx.Done().
  • Connection pool adalah use case klasik — goroutine menunggu sampai ada koneksi tersedia; Signal() dipanggil saat koneksi dikembalikan ke pool.
  • Busy waiting adalah anti-pattern absolutfor { check(); sleep(1ms) } membuang CPU dan menambah latensi; selalu gantikan dengan cond.Wait() atau blocking channel receive.

← Sebelumnya: Immutable Object   Berikutnya: Thread Pool →

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