Channel dengan Buffer

Semantik kapasitas dan backpressure.

Daftar isi

Topik ini membahas channel berpenyangga, yaitu channel yang dibuat dengan kapasitas tertentu sehingga pengiriman tidak selalu harus menunggu penerima. Setelah membacanya, semantik len dan cap, perilaku saat penyangga penuh, serta pertimbangan memilih kapasitas dapat dijelaskan lengkap dengan bukti eksekusi.

Penyangga mengubah waktu tunggu#

Argumen kedua pada make memberi channel penyangga berkapasitas sebanyak argumen tersebut. Penyangga ini dalam istilah pustaka disebut buffer. Pengiriman ke channel berpenyangga langsung selesai selama masih ada slot kosong, dan penerimaan langsung selesai selama penyangga tidak kosong. Hanya penyangga penuh yang membuat pengirim menunggu, dan hanya penyangga kosong yang membuat penerima menunggu. Fungsi len melaporkan jumlah nilai yang sedang berada di dalamnya, sedangkan cap melaporkan kapasitasnya.

Program berikut mengisi antrean tugas berkapasitas tiga tanpa satu pun penerima, lalu menghabiskannya lewat perulangan range.

go main.go
package main

import "fmt"

func main() {
	antrean := make(chan string, 3)

	antrean <- "tugas satu"
	antrean <- "tugas dua"
	antrean <- "tugas tiga"

	fmt.Println("panjang antrean:", len(antrean))
	fmt.Println("kapasitas antrean:", cap(antrean))

	close(antrean)
	for tugas := range antrean {
		fmt.Println("mengerjakan", tugas)
	}
}
text Keluaran
panjang antrean: 3
kapasitas antrean: 3
mengerjakan tugas satu
mengerjakan tugas dua
mengerjakan tugas tiga

Tiga pengiriman pada program di atas selesai tanpa satu pun penerima, karena ketiganya cukup ditampung penyangga. Nilai keluar dengan urutan yang sama dengan urutan masuknya, sebab channel berpenyangga tetap menjaga urutan seperti channel biasa. Pemanggilan close sebelum perulangan tidak membuang isinya, sehingga ketiga tugas tetap diterima sebelum perulangan berhenti. Pola isi lalu tutup inilah cara lazim menyerahkan sekumpulan tugas dan menandai habisnya tanpa menghitung jumlahnya di muka.

Satu kehalusan perlu ditegaskan, yaitu pengiriman yang selesai belum berarti nilai sudah sampai di tangan penerima. Pada channel berpenyangga, pernyataan kirim berhenti menunggu begitu nilainya ditempatkan pada slot kosong, sedangkan nilai itu diam sampai ada penerima yang mengambilnya. Jaminan yang tersisa hanyalah urutan, yaitu nilai yang masuk lebih dahulu pasti keluar lebih dahulu. Sifat ini berbeda dengan channel tanpa penyangga, yang pengirimannya baru selesai pada saat nilai diserahkan langsung kepada penerima.

Membaca len dan cap#

Fungsi len dan cap juga didefinisikan untuk channel. Nilai len adalah jumlah elemen yang sedang mengantre di dalam penyangga pada saat itu, sedangkan cap adalah kapasitas yang ditetapkan pada saat make. Pada channel tanpa penyangga, keduanya selalu bernilai nol karena tidak ada slot penampungan sama sekali. Sepanjang umur channel, cap tidak pernah berubah, sedangkan len bergerak naik dan turun pada setiap pengiriman dan penerimaan.

Program berikut mengamati kedua fungsi itu pada setiap tahap kehidupan sebuah channel berkapasitas tiga, mulai dari keadaan kosong sampai seluruh isinya terkuras.

go len-cap.go
package main

import "fmt"

func main() {
	antrean := make(chan int, 3)
	fmt.Println("awal:", len(antrean), "dari", cap(antrean))

	antrean <- 10
	antrean <- 20
	fmt.Println("dua kiriman:", len(antrean), "dari", cap(antrean))

	antrean <- 30
	fmt.Println("penuh:", len(antrean), "dari", cap(antrean))

	fmt.Println("diterima:", <-antrean)
	fmt.Println("sisa:", len(antrean), "dari", cap(antrean))

	close(antrean)
	for v := range antrean {
		fmt.Println("menguras", v)
	}
	fmt.Println("habis:", len(antrean), "dari", cap(antrean))
}
text Keluaran
awal: 0 dari 3
dua kiriman: 2 dari 3
penuh: 3 dari 3
diterima: 10
sisa: 2 dari 3
menguras 20
menguras 30
habis: 0 dari 3

Keluaran di atas memperlihatkan len naik pada setiap pengiriman, turun satu pada penerimaan, dan kembali nol setelah seluruh isinya terkuras, sedangkan cap tidak bergerak sama sekali. Perlu diingat bahwa angka len berumur sangat pendek, karena berubah pada setiap operasi yang menyentuh channel. Pada program dengan banyak goroutine, nilai itu dapat basi sebelum baris berikutnya dijalankan, sehingga len cocok dipakai untuk pemantauan dan statistik, bukan untuk dasar keputusan yang menuntut ketepatan.

Ketika penyangga penuh#

Perilaku berikutnya terjadi saat kapasitas dilampaui. Program berikut mengisi tiga slot, lalu mencoba pengiriman keempat ketika belum ada penerima.

go kapasitas-penuh.go
package main

import (
	"fmt"
	"time"
)

func main() {
	antrean := make(chan int, 3)

	for i := 1; i <= 3; i++ {
		antrean <- i
	}
	fmt.Println("tiga nilai masuk tanpa penerima")

	selesai := make(chan bool)
	go func() {
		time.Sleep(10 * time.Millisecond)
		<-antrean
		selesai <- true
	}()

	antrean <- 4
	fmt.Println("nilai keempat masuk setelah satu slot dikosongkan")

	<-selesai
	fmt.Println("sisa nilai dalam buffer:", len(antrean))
}
text Keluaran
tiga nilai masuk tanpa penerima
nilai keempat masuk setelah satu slot dikosongkan
sisa nilai dalam buffer: 3

Pengiriman keempat menemukan penyangga penuh, sehingga main tertahan di situ. Baru kemudian goroutine penerima berjalan, mengambil satu nilai, dan pada saat itulah pengiriman keempat selesai. Perhatikan bahwa penerima tidak mencetak apa pun, karena seluruh pencetakan sengaja dilakukan main agar urutan keluaran tetap pasti. Setelah semuanya selesai, len melaporkan tiga, yaitu nilai kedua, ketiga, dan keempat yang masih tertahan di dalam penyangga.

Penungguan itu juga berlangsung beraturan. Pengirim yang menemukan penyangga penuh masuk ke antrean menurut urutan kedatangannya, dan setiap slot yang dikosongkan diberikan kepada pengirim yang menunggu paling lama. Sebaliknya, penerimaan pada channel yang kosong menunggu pengiriman berikutnya, baik nilai itu datang langsung dari pengirim maupun dari slot penyangga yang baru terisi. Aturan antre dua arah ini membuat penyangga tidak mengubah urutan nilai, melainkan hanya menggeser saat pertemuan antara pengirim dan penerima.

Penundaan pengirim saat penyangga penuh disebut tekanan balik (backpressure), yaitu mekanisme yang memaksa laju pengiriman mengikuti laju penerimaan. Sistem yang mengantre pekerjaan dengan cara ini tidak menumpuk tugas tanpa batas, karena pihak yang memproduksi otomatis melambat ketika pihak yang mengonsumsi kewalahan. Tekanan balik justru menghilang apabila kapasitas dibesarkan tanpa alasan, sementara beban yang sesungguhnya tidak berkurang sama sekali.

Dalam praktik, tekanan balik paling sering terlihat pada antrean tugas. Produsen (producer) menghasilkan pekerjaan lebih cepat daripada yang dapat diolah konsumen (consumer), dan tanpa pembatasan apa pun antreannya tumbuh tanpa batas sampai memori habis. Channel berpenyangga memecah persoalan itu dengan ukuran yang tetap, yaitu lonjakan singkat terserap slot yang kosong, sedangkan lonjakan yang berkepanjangan menjadikan produsen menunggu. Nilai len yang dilaporkan channel dapat dipakai untuk mengamati tekanan tersebut dari luar, misalnya mencatat panjang antrean secara berkala, dengan pengertian bahwa angkanya sesaat sebagaimana dijelaskan pada bagian sebelumnya.

Memilih kapasitas#

KapasitasPerilakuKegunaan yang cocok
0, tanpa penyanggakirim dan terima selalu bertemupenyerahan langsung dengan sinkronisasi ketat
kecil, misalnya 3menyerap ledakan singkat, penuh tetap menahan pengirimantrean tugas dengan laju produksi dan konsumsi nyaris seimbang
besarpengirim nyaris tidak pernah menunggumenutupi perlambatan konsumen dengan risiko memori tumbuh

Kapasitas bukan sekadar angka performa, melainkan kebijakan antrean. Kapasitas nol memberi sinkronisasi paling ketat karena pengirim dan penerima selalu bertemu, sedangkan kapasitas besar menyembunyikan perlambatan konsumen sampai memori ikut terasa. Kapasitas yang tepat ditentukan dari laju produksi dan laju konsumsi yang nyata, bukan dari keinginan agar antrean tidak pernah penuh. Satu catatan menyertai semuanya, yaitu penyangga tidak mengubah aturan berbagi data yang dibahas pada topik tentang race condition, karena nilai yang dikirim disalin dan penerima menjadi pemilik tunggal salinan itu.

Perbandingan antara kedua bentuk channel dapat dirangkum sebagai berikut.

AspekTanpa penyanggaDengan penyangga
Pernyataan kirimselesai saat penerima siapselesai saat ada slot kosong
Pernyataan terimaselesai saat pengirim siapselesai saat ada isi
Pertemuan kedua pihakwajib pada setiap serah terimatidak wajib kecuali penyangga penuh atau kosong
Kegunaan yang cocokjaminan serah terima yang ketatmenyerap lonjakan laju yang singkat

Kedua bentuk pada tabel bukan alat yang saling menggantikan, melainkan dua kebijakan serah terima. Channel tanpa penyangga memberi bukti bahwa penerima sudah ada pada saat pengiriman selesai, sedangkan channel berpenyangga memberi kelenturan laju dengan mengorbankan kepastian itu. Apabila tahap penerima perlu dijamin siap pada setiap serahan, channel tanpa penyangga yang lebih tepat, sedangkan apabila lonjakan laju singkat perlu diserap, penyangga kecil memberi manfaatnya dengan biaya memori yang terukur.

Kesalahan umum#

  • Mengira pengiriman tidak pernah menunggu pada channel berpenyangga. Penyangga penuh tetap menahan pengirim, sebagaimana ditunjukkan program kedua di atas.
  • Menghitung len sebagai jumlah tugas yang akan datang. len hanya melaporkan isi pada saat itu, dan nilainya berubah pada setiap pengiriman dan penerimaan.
  • Lupa menutup channel yang dipakai dengan range. Perulangan menunggu nilai berikutnya selamanya, dan apabila tidak ada goroutine lain yang hidup, program berakhir deadlock.

Ringkasan#

  • Argumen kedua make memberi kapasitas penyangga, pengiriman hanya menunggu saat penyangga penuh, sedangkan len melaporkan isi sesaat dan cap melaporkan kapasitas tetap.
  • Channel berpenyangga tetap menjaga urutan, dan nilai yang tertahan tetap diterima setelah close.
  • Penyangga penuh menciptakan tekanan balik yang menyamakan laju pengiriman dengan laju penerimaan.
  • Kapasitas adalah kebijakan antrean, bukan pengganti sinkronisasi, dan besar semata hanya menunda persoalan.

Referensi#