Programmer Go 4-chan
(dolthub.com)- DoltHub sengaja membuat contoh bercanda yang menumpuk channel, yang umum dipakai dalam concurrency Go, secara berlebihan: mengirim channel melalui channel
- Di kode warisan yang benar-benar mereka terima, ada
chan chan struct{}; itu dipakai dalam pola fan-out yang meneruskan channel baru ke goroutine worker, tetapi sulit ditalar dan dikelola sehingga akhirnya ditulis ulang - Contoh ini memperluas lelucon “4-star programmer”
int****dari keluarga bahasa C kechandi Go, dengan_4chan:= make(chan chan chan chan int)sebagai channel tingkat teratas - Dengan
factor = 3, program membuat producer dan consumer bercabang di tiap lapisan channel, lalu menjumlahkan nilaiintterakhir untuk mencetak 243, yaitu 3 pangkat 5 - Dalam praktik, pendekatan ini tidak cocok karena sulit diimplementasikan dan di-debug, rumit menangani penutupan channel, membutuhkan
sync.WaitGroup, serta berisiko menyebabkan kebocoran goroutine; contoh ini mengandalkantime.Sleep()alih-alih logika terminasi
Kasus penumpukan channel dari Dolt
- DoltHub menulis Dolt, database SQL berversi pertama di dunia, dengan Go
- Seperti codebase Go pada umumnya, mereka menggunakan channels dan goroutines untuk mengimplementasikan eksekusi konkuren
- Karena pemrograman konkuren memang sulit, channel dan goroutine biasanya dipakai dengan cara yang sederhana dan intuitif
- Pada suatu waktu, kode yang diambil dari proyek open source lain berisi channel yang mengirim channel seperti berikut
var c chan chan struct{}
- Struktur ini adalah cara meneruskan channel antar-goroutine untuk mengimplementasikan pola fan-out pada goroutine worker
- Channel perantara berperan sebagai broker yang meneruskan channel yang baru dibuat kepada worker yang melakukan pekerjaan sebenarnya
- Kodenya berjalan, tetapi sulit ditalar dan ditangani, terutama jika mempertimbangkan kebocoran goroutine
- Kode tersebut ditulis ulang dan
chan chan struct{}pun hilang
Versi Go dari lelucon “4-star programmer”
- Pada masa ketika C dan bahasa turunannya banyak digunakan, ada lelucon “4-star programmer” tentang pemula yang kesulitan memahami pointer
- Contoh tipikalnya adalah kode yang memakai beberapa tingkat indireksi pointer seperti
int**** - Karena Go juga banyak diturunkan dari C, kode serupa bisa ditulis dengan pointer
- Meneruskan
*int,**int,***int,****intsecara berurutan - Jika fungsi terakhir menjalankan
****i = 100, program akan mencetaki is now 100
- Meneruskan
- Karena Go memiliki
chan, yang tidak ada di C, lelucon yang sama bisa diperluas menjadi indireksi channel
Menghitung pangkat 5 dengan channel 4 tingkat
- Channel tingkat teratas dideklarasikan seperti berikut
_4chan := make(chan chan chan chan int)
- Identifier Go tidak bisa diawali angka, jadi contoh ini menggunakan nama
_4chan - Nilai yang dikirim ke
_4chanadalah channel 3 tingkat_3chan := make(chan chan chan int)
- Dengan cara yang sama, hierarki terus turun hingga akhirnya mencapai channel nilai, yaitu
chan int - Di setiap lapisan indireksi, producer dibuat berdasarkan konstanta
factor- Dalam contoh,
const factor = 3 sendChanChanChanmemulai producer 3-channel sebagai goroutine
- Dalam contoh,
- Di sisi consumer juga, channel yang masuk di tiap lapisan diterima lalu consumer tahap berikutnya dimulai sebanyak
factorreceiveChanChanChanmenerima_3chandari_4chan, lalu memulai consumer 3-channel
Pengiriman dan penjumlahan nilai di lapisan terakhir
- Pada lapisan paling bawah, yang dikirim bukan lagi channel, melainkan nilai
intsebenarnya - Fungsi
sendmengirim_1chanke_2chan, lalu memulai int producer sebanyakfactor - Setiap int producer kembali membuat goroutine sebanyak
factoruntuk menjalankan_1chan <- 1 - Consumer menambahkan integer yang diterima ke
sumglobalsumdideklarasikan sebagaiatomic.Int32receive(c chan int)menerima nilai dari channel dan menjalankansum.Add(int32(s))
Hasil eksekusi dan jumlah percabangan
- Program lengkap membuat
_4chan, memulai lapisan pengirim dan penerima masing-masing sebagai goroutine, lalu menunggu selama500 * time.Millisecond - Output contohnya adalah sebagai berikut
3 ^ 5: 243
- Program ini adalah contoh tergeneralisasi untuk menghitung pangkat 5 dari sebuah angka dengan cara yang sedistribusikan mungkin
- Contoh yang bisa dijalankan tersedia di Go Playground, dan versi dengan penyorotan sintaks ada di GitHub Gist
- Jika memakai
factoryang lebih besar, waktuSleepmungkin perlu diperpanjang agar eksekusi bisa selesai - Jika logging dinyalakan, jumlah percabangan di tiap lapisan produksi dan konsumsi channel bisa diperiksa
starting 3chan producer: 3 kalistarting 2chan producer: 9 kalistarting 3chan consumer: 9 kalistarting 2chan consumer: 27 kalistarting chan producer: 27 kalistarting 1chan consumer: 81 kalistarting int producer: 81 kalisending int: 243 kalireceived int: 243 kali
Mengapa harus dihindari dalam kode produksi
- Pendekatan ini sulit diimplementasikan dan di-debug untuk dipakai dalam kode nyata
- Jika channel dikirim melalui channel, menentukan kapan tiap channel harus ditutup menjadi sulit
- Dalam kasus penggunaan nyata, channel harus ditutup, tetapi untuk menambahkan logika terminasi, perlu dilacak apakah semua pengiriman channel sudah selesai
- Untuk mengimplementasikan penanganan terminasi,
sync.WaitGroupharus ditambahkan di berbagai tempat, dan itu akan membuat contoh bercanda ini sulit dibaca - Contoh akhirnya disederhanakan dengan memakai
time.Sleep()alih-alih logika terminasi, dan dengan meninggalkan banyak kebocoran goroutine
1 komentar
Opini Hacker News
Dari sudut pandang ilmuwan yang bekerja dekat dengan para software engineer profesional sungguhan, banyak hal yang mereka lakukan terlihat seperti ini, jadi sangat sulit memahami kenapa mereka melakukannya
Saya pernah melihat satu baris kode harus melewati 4 fungsi antarmuka secara berurutan sebelum benar-benar dipanggil, dan fungsi-fungsi itu tersebar di file berbeda dalam folder berbeda
Akibatnya melelahkan untuk membaca apa yang sebenarnya dilakukan kode itu, dan setelah masuk beberapa tahap, saya mulai ragu apakah saya sedang melihat tempat yang benar, atau apakah saya akan pernah sampai ke bagian yang benar-benar melakukan komputasi
Kesan bahwa ini berlebihan dan membingungkan itu tidak salah, dan saat pertama kali menulis kode yang “menarik”, secara teknis itu bisa tampak rumit bahkan elegan, tetapi di dalam perangkat lunak yang harus benar-benar berkembang, itu menjadi mimpi buruk teknis
Saya pernah menghabiskan hampir 2 tahun membereskan penyalahgunaan channel di kode Go; masalahnya, channel sebenarnya jarang benar-benar diperlukan, tetapi di awal mudah dipakai untuk banyak hal
Patokan untuk menilai apakah channel dipakai dengan benar adalah: bisakah dijawab tidak untuk “apa ini tidak bisa dilakukan dengan pemanggilan fungsi langsung?”, “apa ini tidak bisa dilakukan dengan wait group atau mutex?”, dan bisakah dijawab ya untuk “apakah manfaat konkurensi/paralelisme cukup besar untuk membenarkan kerumitan debugging kode konkurensi?”
Kadang ada kekurangan yang sampai terasa menakutkan pada hal-hal yang seharusnya membutuhkan pemahaman dan kompetensi
Saat kuliah S1, saya pernah pair programming sekitar 30 menit dengan seorang doktor ilmu komputer, dan itu cukup membuka mata
Dia sama sekali tidak memahami perangkat lunak, sampai-sampai mengkritik bahwa struktur data di standard library tidak mengecek apakah ukurannya bernilai negatif atau tidak
Namun, kadang struktur seperti ini memang punya alasan. Terkadang benar-benar masuk akal, dan terkadang ini cara untuk menghadapi codebase gila yang ditinggalkan orang-orang sebelumnya
Saya pernah membaca kode yang ditulis untuk makalah penelitian; matematika teoretis biasanya sudah sulit dipahami, tetapi kalau masuk ke kode untuk melihat logikanya, sering kali malah jauh lebih parah dan nyaris tidak bisa dikenali
Pada akhirnya kita terbiasa dengan cara kita sendiri, dan cara lain terasa asing
Meski begitu, lapisan indireksi tambahan biasanya dibenarkan dalam arsitektur keseluruhan, dan jika memang masuk akal, nilainya bisa sulit dilihat jika hanya dilihat secara lokal
“Sentuhan ringan dan keheningan bahasa pemrograman awal selalu menyenangkan. Teksnya tidak banyak, tetapi banyak hal dikerjakan. Program-program lama terbaca bukan seperti perdebatan dengan compiler, melainkan seperti percakapan tenang antara peneliti yang fasih dan rekan mesin yang terlatih baik. Siapa yang menyangka bahwa kecanggihan akan membeli kebisingan seperti ini?” — Dick Gabriel
Kita melihat dua ekstrem. Codebase yang terlalu tersebar karena terlalu banyak abstraksi, dan codebase yang sama sekali tanpa abstraksi sehingga lebih mirip skrip untuk sekadar mencapai tujuan, sama-sama sulit dikerjakan
Di antara skrip Python, JS, dan PHP, banyak yang ditulis dengan sikap “sudah, kasih saja hasil yang saya mau”, dan bagi orang yang bekerja dengan kode setiap hari, abstraksi dibutuhkan untuk membantu kolaborasi dan ketahanan
Meme di bagian awal benar-benar lucu bagi saya sebagai mantan programmer C yang sedang dalam masa pemulihan
Menyenangkan melihat contoh bahasa dipelintir seperti ini, dan C punya banyak sekali kesempatan untuk itu, jadi menarik melihat hal serupa juga di Go
Katanya ini “lelucon pemrograman lama dari masa ketika C dan bahasa turunannya mendominasi”, tetapi kita sebenarnya masih hidup di masa itu
Ironis melihat hal ini dikritik di thread ini sebagai contoh khas abstraksi berlebihan
Alasan kita jarang melihat variabel dengan tiga bintang di C bukan karena rantai pointer itu langka, melainkan karena kebutuhan untuk memanipulasi lebih dari dua tingkat sekaligus memang tidak umum
Dalam bahasa seperti Python, Java, JavaScript, yang pada dasarnya kebanyakan sudah mirip pointer, jelas ada rantai pointer yang panjangnya jauh melebihi 4
Bagian yang dalam biasanya disembunyikan di dalam struct yang tidak perlu kita pikirkan isinya, yaitu diabstraksikan
Channel juga kira-kira menjalankan peran yang dilakukan pointer dalam kode sekuensial, tetapi untuk kode konkurensi, jadi kalau ada cacat fatal di sini, mungkin masalahnya bukan abstraksi berlebihan, melainkan justru kurangnya abstraksi
Tetap saja, tanpa mengenal codebase-nya, bisa jadi
chan chanmemang abstraksi yang tepat untuk apa yang ingin mereka tulis. Dalam bahasa yang sangat berorientasi konkurensi seperti Erlang, mengirim PID ke PID lain untuk mengidentifikasi proses yang akan menerima respons adalah hal yang sangat umumPadahal itu bukan seperti itu, melainkan alamat dari array string. Memang begitu karena ini C, dan bahkan sekarang, hampir 20 tahun kemudian, saya masih menganggap itu solusi yang paling intuitif
Untuk membela rekan kerja saya, mungkin saat itu tidak ada komentar. Bagian itu memang tanggung jawab saya
Ini mengingatkan saya pada karya klasik lintas zaman dari Buena Vista Social Club https://www.youtube.com/watch?v=o5cELP06Mik
chan chan Valueatauchan struct{resp chan Value}adalah pola yang memang pernah dipakai dalam situasi yang sangat spesifikBisa saja memakai message bus, tetapi kalau begitu kita juga harus menangani message bus itu sendiri
chan chancukup sering dipakai. Saat mengirim pesan ke server internal atau aktor, channel untuk menerima balasan juga ikut dimasukkan ke dalam pesanDalam praktiknya, alih-alih
chan chanliteral yang bisa dicari dengan grep, bentuknya lebih sering sepertichan struct { ... sesuatu yang berisi channel ... }, tetapi prinsipnya samaIni pola yang sangat berguna dan menurut saya termasuk salah satu dasar Go
Namun, setahu saya saya belum pernah memakai
chan chan chanMengganti semua ini dengan message bus itu berlebihan. Karakteristik performanya juga sangat berbeda, dan karena channel Go lebih mirip konstruksi internal di dalam proses sistem operasi, untuk komunikasi intra-proses tidak ada nilai untuk “meng-upgrade”-nya menjadi message bus
[0]: https://github.com/adonovan/gopl.io/blob/master/ch8/chat/cha...
Channel dari channel adalah pola yang wajar, tetapi biasanya lebih sering muncul dalam bentuk channel yang membawa nilai struct, dengan field channel di dalam tipe struct tersebut
Ini bisa dipakai untuk mengirim permintaan yang akan selesai nanti, lalu menunggu nilainya tanpa memedulikan urutan sehingga bisa menghindari head-of-line blocking
Misalnya, mengirim permintaan lewat channel seperti
type request struct { params, reply chan response }, lalu worker memproses pekerjaannya dan menaruh hasilnya ke channelreplyNamun, yang terasa berguna hanya sampai dua tingkat, dan saya belum pernah melihat use case yang membutuhkan
chan chan chanAda blog contoh tandingan yang mengimplementasikan dynamic dispatch dengan channel yang mengirim channel. Bukan Go melainkan Limbo, tetapi konsepnya sama. Mungkin kompleksitasnya justru membuktikan poin tersebut https://ipn.caerwyn.com/2007/07/lab-78-dynamic-dispatch.html...
Ini mengingatkan pada “My favorite Erlang Program” karya Joe Armstrong
https://joearms.github.io/published/2013-11-21-My-favorite-e...