1 poin oleh GN⁺ 2024-08-29 | 1 komentar | Bagikan ke WhatsApp
  • 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 ke chan di 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 nilai int terakhir 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 mengandalkan time.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, ****int secara berurutan
    • Jika fungsi terakhir menjalankan ****i = 100, program akan mencetak i is now 100
  • 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 _4chan adalah 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
    • sendChanChanChan memulai producer 3-channel sebagai goroutine
  • Di sisi consumer juga, channel yang masuk di tiap lapisan diterima lalu consumer tahap berikutnya dimulai sebanyak factor
    • receiveChanChanChan menerima _3chan dari _4chan, lalu memulai consumer 3-channel

Pengiriman dan penjumlahan nilai di lapisan terakhir

  • Pada lapisan paling bawah, yang dikirim bukan lagi channel, melainkan nilai int sebenarnya
  • Fungsi send mengirim _1chan ke _2chan, lalu memulai int producer sebanyak factor
  • Setiap int producer kembali membuat goroutine sebanyak factor untuk menjalankan _1chan <- 1
  • Consumer menambahkan integer yang diterima ke sum global
    • sum dideklarasikan sebagai atomic.Int32
    • receive(c chan int) menerima nilai dari channel dan menjalankan sum.Add(int32(s))

Hasil eksekusi dan jumlah percabangan

  • Program lengkap membuat _4chan, memulai lapisan pengirim dan penerima masing-masing sebagai goroutine, lalu menunggu selama 500 * 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 factor yang lebih besar, waktu Sleep mungkin perlu diperpanjang agar eksekusi bisa selesai
  • Jika logging dinyalakan, jumlah percabangan di tiap lapisan produksi dan konsumsi channel bisa diperiksa
    • starting 3chan producer: 3 kali
    • starting 2chan producer: 9 kali
    • starting 3chan consumer: 9 kali
    • starting 2chan consumer: 27 kali
    • starting chan producer: 27 kali
    • starting 1chan consumer: 81 kali
    • starting int producer: 81 kali
    • sending int: 243 kali
    • received 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.WaitGroup harus 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

 
GN⁺ 2024-08-29
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

    • Ini benar-benar praktik yang buruk, dan lebih mirip cara engineer junior yang terlalu bersemangat menulis perangkat lunak
      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?”
    • Ada yang lebih parah. Ini bukan contoh yang terlalu dilebih-lebihkan: https://github.com/EnterpriseQualityCoding/FizzBuzzEnterpris...
      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
    • Di salah satu ujung spektrum coder ada ilmuwan, di ujung lainnya ada software engineer. Satu-satunya yang bisa menyelamatkan kita adalah keseimbangan
      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
    • Overengineering adalah penyebab yang umum. Solusi sederhana sering tersembunyi dan sulit ditemukan
      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
    • Sebaliknya, saya setuju bahwa ketika software engineer tidak terlalu paham, mereka sering membawa abstraksi terlalu jauh, tetapi saya juga tidak memandang kode yang dibuat oleh orang yang bukan software engineer profesional sebagai sesuatu yang secara khusus lebih baik
      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 chan memang 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 umum

    • Di awal karier, saya pernah dimarahi karena sebuah commit dengan tiga bintang. Kurang lebih komentarnya: siapa yang butuh pointer ke pointer ke pointer?
      Padahal 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 Value atau chan struct{resp chan Value} adalah pola yang memang pernah dipakai dalam situasi yang sangat spesifik
    Bisa saja memakai message bus, tetapi kalau begitu kita juga harus menangani message bus itu sendiri

    • chan chan cukup sering dipakai. Saat mengirim pesan ke server internal atau aktor, channel untuk menerima balasan juga ikut dimasukkan ke dalam pesan
      Dalam praktiknya, alih-alih chan chan literal yang bisa dicari dengan grep, bentuknya lebih sering seperti chan struct { ... sesuatu yang berisi channel ... }, tetapi prinsipnya sama
      Ini pola yang sangat berguna dan menurut saya termasuk salah satu dasar Go
      Namun, setahu saya saya belum pernah memakai chan chan chan
      Mengganti 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
    • Mungkin polanya mirip dengan server chat konkuren di gopl. Awalnya agak mengejutkan, tetapi tetap cukup enak dibaca
      [0]: https://github.com/adonovan/gopl.io/blob/master/ch8/chat/cha...
    • Saya juga pernah memakainya. Ini berguna untuk mengimplementasikan semantik mirip Promise di Go
  • 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 channel reply
    Namun, yang terasa berguna hanya sampai dua tingkat, dan saya belum pernah melihat use case yang membutuhkan chan chan chan

  • Ada 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...

    • Sebagai tambahan untuk yang penasaran, Limbo adalah bahasa pendahulu Go dan digunakan di sistem operasi Inferno, keturunan Plan 9
  • Ini mengingatkan pada “My favorite Erlang Program” karya Joe Armstrong
    https://joearms.github.io/published/2013-11-21-My-favorite-e...