2 poin oleh GN⁺ 2023-07-18 | 1 komentar | Bagikan ke WhatsApp
  • Di Go, ada pola konkurensi yang terasa janggal jika hanya memakai goroutine dan channel; coroutine adalah usulan yang membantu menata struktur program dengan menyerahkan alur eksekusi secara eksplisit tanpa paralelisme
  • Coroutine saling menyerahkan kendali eksekusi lewat resume dan yield, dan karena hanya satu yang berjalan pada satu waktu, race pada data bersama bisa dihindari sekaligus menjadikan titik perpindahan sebagai titik sinkronisasi
  • Python generator dan CLU iterator mirip dengan coroutine, tetapi posisi yield dibatasi, sehingga jika penelusuran pohon bertingkat ala Lua dipindahkan apa adanya, sebagian nilai bisa hilang
  • coro.New di Go juga dapat diekspresikan dengan channel dan goroutine, sedangkan coro.Pull mengubah push iterator menjadi pull iterator yang mengeluarkan satu nilai setiap kali dipanggil
  • Implementasi berbasis channel memakan sekitar 190ns per perpindahan, sedangkan perpindahan langsung di runtime dapat menurunkannya hingga sekitar 20ns per perpindahan dan sekitar 40ns per nilai untuk coro.Pull, dengan target cukup rendah agar tidak menjadi bottleneck dalam penggunaan nyata

Model eksekusi coroutine

  • Coroutine terlihat seperti pemanggilan fungsi, tetapi berjalan di stack yang berbeda dan tidak berjalan bersamaan
    • Meski F memulai G, G tidak langsung berjalan; F harus secara eksplisit memanggil resume agar G dieksekusi
    • Saat berjalan, G kapan saja dapat mengembalikan kendali ke F dengan yield
    • Jika G melakukan return, ia akan dibersihkan, dan F menerima sinyal bahwa G tidak boleh lagi di-resume
  • Dalam model ini, hanya satu coroutine pada satu waktu yang berjalan, sementara pemanggil menunggu di stack lain
  • Perpindahan eksekusi hanya terjadi di titik-titik tertentu dalam program, sehingga beberapa alur berjalan bergantian dengan cara yang terkoordinasi

Coroutine lewat contoh Lua

  • Contoh Lua 5 membandingkan apakah dua pohon biner dengan struktur berbeda memiliki urutan nilai yang sama
    • t1 dan t2 berisi 1, 2, 3, 4, 5
    • t3 berisi 1, 2, 3, 4, 6
  • visit(t) menelusuri pohon secara inorder dan mengeluarkan setiap nilai melalui coroutine.yield(t.value)
  • Fungsi pembanding membuat dua coroutine visit lalu memanggil coroutine.resume secara bergantian untuk membaca nilai berikutnya
    • Jika status selesai kedua coroutine berbeda atau nilainya berbeda, hasilnya false
    • Jika keduanya sama-sama selesai, hasilnya true
  • Kode Lua yang lebih idiomatis memakai coroutine.wrap untuk mendapatkan fungsi next yang menyembunyikan objek coroutine
    • Ketika coroutine selesai, fungsi next akan mengembalikan nil
    • Kode lengkapnya ada di Gist

Keterbatasan Python generator dan CLU iterator

  • Python generator tampak seperti coroutine Lua, tetapi modelnya tidak sama
  • Jika contoh Lua dipindahkan langsung ke Python, visit(t['left']) tidak benar-benar menjalankan penelusuran, melainkan hanya membuat objek generator lalu membuangnya
    • Jika yield ada di badan fungsi, def visit tidak lagi mendefinisikan fungsi biasa, melainkan generator
    • Contoh terjemahan sederhana hanya mencetak 4 dari pohon, dan kehilangan 1, 2, 3, 5
  • Kode Python yang benar harus mengiterasi generator bertingkat secara eksplisit lalu melakukan yield lagi
    • yield from di Python 3.3 membuat pola ini jauh lebih sederhana
  • Objek Python generator hanya menyimpan status dari satu pemanggilan visit
    • Nilai variabel lokal dan baris yang sedang dieksekusi disimpan di objek generator
    • Saat dilanjutkan, status itu kembali dinaikkan ke stack pemanggilan, lalu keluar lagi ke objek generator saat yield
    • yield hanya bisa dilakukan di frame pemanggilan tingkat teratas
  • CLU menyebut abstraksi ini sebagai iterator, dan secara statis membedakan iter dan proc
    • Berkat informasi tipe, compiler dapat mendiagnosis penggunaan yang keliru ketika iterator dipanggil seolah fungsi biasa
    • Makalah tahun 1977 oleh Barbara Liskov dkk., “Abstraction Mechanisms in CLU”, menjelaskan bahwa iterator adalah bentuk coroutine yang dibatasi sehingga dapat diimplementasikan hanya dengan stack program

Perbedaan coroutine, thread, dan generator

  • Ketiga konsep ini sama-sama menyediakan bentuk tertentu dari konkurensi, tetapi kekuatan dan biayanya berbeda
  • Coroutine

    • Menyediakan konkurensi tanpa paralelisme
    • Jika satu coroutine sedang berjalan, coroutine yang melanjutkannya atau yang telah menyerahkan kendali kepadanya tidak berjalan
    • Karena titik perpindahannya eksplisit, race dalam berbagi data tidak muncul
    • Perpindahan seperti pemanggilan coroutine.resume atau next menjadi titik sinkronisasi yang membentuk happens-before edge
    • Karena dijadwalkan secara eksplisit tanpa sistem operasi, perpindahannya bisa berada kira-kira di bawah 10ns
  • Thread

    • Lebih kuat daripada coroutine, dan kekuatan tambahannya adalah paralelisme
    • Konsekuensinya adalah overhead penjadwalan, context switch yang lebih mahal, serta perlunya bentuk preemption tertentu
    • Perpindahan thread pada umumnya berada di kisaran beberapa mikrodetik
  • Go goroutine

    • Dalam klasifikasi ini, lebih dekat ke thread murah
    • Go runtime menangani sebagian penjadwalan sehingga perpindahannya mendekati beberapa ratus ns
    • Seperti thread, ia menyediakan paralelisme dan preemption
    • Lightweight thread baru di Java pada dasarnya juga sama dengan goroutine
  • Generator

Kasus penggunaan yang membutuhkan coroutine di Go

  • Library konkurensi Go yang ada saat ini tidak secara langsung menyediakan pola coroutine
  • Goroutine sering kali cukup mirip, tetapi karena ada paralelisme dan preemption, hasilnya bisa berbeda dari coroutine
  • Presentasi Rob Pike tahun 2011, “Lexical Scanning in Go”, membahas desain awal lexer dan parser pada paket text/template
    • Lexer dan parser berjalan di goroutine terpisah dan dihubungkan dengan channel
    • Ini adalah struktur yang meniru pasangan coroutine secara tidak sempurna
    • Lexer melihat token berikutnya lebih dulu saat parser sedang memproses token terbaru
    • Generator tidak cukup untuk lexer yang perlu melakukan yield nilai dari beberapa fungsi
    • Paralelisme pada goroutine menimbulkan race, dan pada akhirnya desainnya diubah menjadi menyimpan status lexer dalam objek
    • Jika ada coroutine yang tepat, race bisa dihindari dan akan lebih efisien daripada goroutine
  • Sebagai kasus penggunaan masa depan, ada iterasi koleksi generic
    • Di Go, pernah ada pembahasan tentang dukungan range untuk fungsi
    • Ini dapat mendorong koleksi dan penulis abstraksi untuk menyediakan fungsi iterator ala CLU
  • Di Go saat ini pun, push iterator bisa diimplementasikan dengan nilai fungsi
    • Contoh: func (t *Tree[V]) All(yield func(v V))
    • Saat ini bisa dipanggil seperti t.All(func(v V) { fmt.Println(v) })
    • Di masa depan, bentuk for v := range t.All mungkin dimungkinkan
  • Masalahnya muncul pada penelusuran yang tidak cocok dengan satu loop for
    • Misalnya, seperti perbandingan pohon biner yang perlu menyisipkan dua penelusuran satu sama lain
    • Coroutine dapat mengubah push iterator seperti (*Tree).All menjadi pull iterator yang mengembalikan satu nilai setiap kali dipanggil

coro.New yang diekspresikan dengan Go murni

  • Jika coroutine ditambahkan ke Go, hal itu harus dimungkinkan tanpa perubahan bahasa, serta dapat dipahami dan diimplementasikan dengan kode Go biasa
  • coro.New yang sederhana dapat diekspresikan dengan channel dan goroutine
    • cin meneruskan nilai input
    • cout mengembalikan nilai output
    • resume mengirim nilai ke cin dan menunggu hasil dari cout
    • goroutine baru awalnya terblokir di <-cin, sehingga tidak ada peluang eksekusi paralel
  • Jika yield ditambahkan, f dapat mengeluarkan nilai saat berjalan, dan pemanggil dapat memasukkan nilai lagi pada resume berikutnya
    • yield(out) mengirim nilai ke cout dan menunggu input berikutnya dari cin
    • Ini juga merupakan pasangan send-receive, jadi tidak ada paralelisme
  • Pola komunikasi ini membatasi goroutine agar berperilaku seperti coroutine
    • Secara nyata ini tetap goroutine, tetapi resume dan yield berperan sebagai operasi perpindahan

Contoh parser string

  • Masalah dalam “Storing Data in Control Flow” adalah menjalankan func parseQuoted(read func() byte) bool dalam alur kontrol terpisah, lalu memasok byte satu per satu melalui metode Write
  • Dengan coro.New, ini dapat ditulis pada level yang lebih tinggi dibanding implementasi sementara berbasis channel pada tulisan sebelumnya
    • Init mendefinisikan fungsi coparse
    • read melakukan yield atas NeedMoreInput, lalu mengembalikan byte yang dikirim pemanggil
    • Hasil boolean dari parseQuoted(read) diubah menjadi BadInput atau Success
    • p.resume(0) menjalankan hingga read pertama dari parseQuoted
    • Write(c byte) menjadi pembungkus tipis yang memanggil p.resume(c)
  • Kode lengkap ada di Go Playground

Contoh sieve bilangan prima

  • Concurrent prime sieve karya Doug McIlroy adalah pipeline dengan satu coroutine untuk setiap bilangan prima p
    • Setiap filter menerima angka dari tetangga kiri dan, jika tidak habis dibagi p, meneruskannya ke tetangga kanan
    • Counter di ujung kiri memasok 2, 3, 4, ...
    • Coroutine output di ujung kanan membaca bilangan prima untuk dicetak lalu membuat coroutine filter baru
  • counter adalah fungsi yang membungkus loop yang melakukan yield nilai dengan coro.New
    • more bool menyampaikan apakah pembuatan harus dilanjutkan
    • yield(i) mengeluarkan nilai lalu menerima status lanjut berikutnya
  • filter(p, next) mengambil nilai dari next(true) milik coroutine kiri, lalu hanya melakukan yield(n) saat n%p != 0
  • main menyimpan output pipeline saat ini di next
    • Membaca bilangan prima p
    • Mencetak p
    • Menambahkan filter baru di sisi kanan pipeline untuk membuang kelipatan p
  • Relasi pemanggilan antar-coroutine dapat berubah saat eksekusi
    • yield pertama dari counter menuju main, tetapi yield berikutnya menuju filter-2
    • Output pertama dari setiap filter-p menuju main sebagai bilangan prima berikutnya, dan output berikutnya menuju filter berikutnya
  • Kode lengkap ada di Go Playground

Hubungan antara goroutine dan coroutine

  • Alur kontrol yang dibuat di sini, secara ketat, adalah goroutine
    • Ia dapat melakukan semua hal yang bisa dilakukan goroutine biasa, seperti mutex, channel, menunggu system call, dan sebagainya
  • coro.New membuat goroutine yang dapat menggunakan operasi perpindahan coroutine di dalam yield dan resume
  • Pernyataan go membuat alur kontrol baru yang konkuren dan paralel, sedangkan coro.New membuat alur kontrol baru yang konkuren tetapi tidak paralel
    • Jika 10 pernyataan go dijalankan, maka 11 goroutine termasuk main dapat berjalan secara bersamaan
    • Jika coro.New dipanggil 10 kali, alur kontrol menjadi 11, tetapi paralelisme program tetap sama dan hanya satu yang berjalan pada satu waktu
  • Goroutine mana yang berperan sebagai coroutine “tidak paralel” dapat berubah selama eksekusi
    • Ini sama seperti goroutine mana yang sedang melakukan send atau receive pada channel dapat berubah saat eksekusi

resume yang lebih tangguh

  • coro.New awal akan menyebabkan deadlock jika resume dipanggil setelah fungsi selesai
  • Untuk memperbaikinya, resume mengembalikan bool bersama hasilnya
    • true berarti hasil berasal dari yield
    • Jika fungsi mengembalikan, resume mengembalikan nilai return dan false
    • Jika resume dipanggil setelah coroutine selesai, ia mengembalikan zero value dan false
  • Variabel running melacak apakah f sedang berjalan
    • resume dan coroutine berjalan saling bergantian, jadi pembagian running bukan race condition
  • Contoh mencetak "hello" true, "world" true, "done" false, "" false

Mengubah iterator dengan coro.Pull

  • coro.Pull mengubah push iterator menjadi pull iterator
  • Bentuk push iterator input adalah sebagai berikut
    • push func(yield func(V) bool)
    • Nilai return boolean dari yield menunjukkan apakah iterasi harus dilanjutkan
  • Bentuk pull iterator yang dituju adalah sebagai berikut
    • pull func() (V, bool)
    • Seperti channel receive atau map lookup, ia mengembalikan nilai dan apakah iterasi telah selesai
  • Untuk penghentian dini, Pull mengembalikan bukan hanya pull tetapi juga stop
  • Implementasinya cukup dengan membuat wrapper kecil yang menjalankan push iterator dengan coro.New
    • pull memanggil resume(true)
    • stop memanggil resume(false)
  • Metode All pada tree diubah agar menggunakan hasil bool dari yield
    • Traversal kiri, yield nilai saat ini, dan traversal kanan dirangkai dengan && untuk meneruskan penghentian dini
  • Fungsi pembanding tree membuat dua coro.Pull dan membandingkan nilai satu per satu
    • defer stop1() dan defer stop2() menghentikan coroutine saat keluar lebih awal
    • Jika nilai atau status selesai berbeda, hasilnya false
    • Jika keduanya selesai, hasilnya true
  • Kode lengkap ada di Go Playground

propagasi panic dan pembatalan

  • Panic yang terjadi di coroutine dapat dikembalikan ke pemanggil yang paling baru me-resume coroutine tersebut
    • Pada goroutine biasa, sulit menentukan harus memberi tahu goroutine yang mana dan apakah goroutine itu siap menerimanya
    • Pada coroutine, pemanggil sedang menunggu terblokir di resume, sehingga target penyampaian panic jelas
  • Implementasinya mengirim pesan yang berisi nilai atau panic melalui cout
    • defer pada coroutine baru menangkap panic
    • resume yang sedang menunggu akan mem-panic ulang dengan nilai panic yang sama
  • Dalam contoh, coroutine me-yield "hello" lalu panic dengan "world"
    • Panic dipropagasikan ke goroutine main, dan pada stack terlihat seolah terjadi di pemanggilan resume
    • Kode lengkap ada di Go Playground
  • Fungsi cancel ditambahkan untuk memberi tahu coroutine ketika pemanggil berhenti lebih awal
    • cancel mirip dengan resume, tetapi membuat yield panic alih-alih mengembalikan nilai
    • Panic pembatalan menggunakan error wrapper unik yang memenuhi ErrCanceled
    • Panic yang dipicu cancel tidak dipropagasikan lagi, tetapi jika coroutine menghasilkan panic lain saat pembatalan, panic itu akan dipropagasikan
    • Jika resume belum pernah dipanggil, cancel membuat f sama sekali tidak dijalankan
  • Untuk menghentikan iterator, bool yang eksplisit lebih jelas daripada panic, jadi Pull tetap memakai penghentian berbasis bool

meninjau kembali saringan bilangan prima: pembersihan dan propagasi error

  • Pada API baru, counter dan filter sama-sama mengembalikan fungsi resume dan fungsi cancel
  • primes(n int) membuat counter lalu mendaftarkan defer cancel()
    • Membaca dan mencetak setiap bilangan prima
    • Setiap kali menambahkan filter baru, cancel untuk filter tersebut juga didaftarkan dengan defer
  • Ketika fungsi mendapatkan n bilangan prima lalu mengembalikan hasil, pemanggilan cancel yang tertunda akan membersihkan coroutine yang telah dibuat
  • Jika suatu coroutine panic, panic itu akan dipropagasikan ke coroutine yang sedang menunggu
    • Jika coroutine yang di-resume langsung oleh primes lewat next, panic akan kembali ke primes
    • Jika filter me-resume coroutine lewat next, panic akan naik melalui rantai filter hingga ke p := next(true) di primes
    • Setelah itu, cancel yang ditunda di primes akan membersihkan coroutine yang tersisa
  • Kode lengkap ada di Go Playground

bentuk API final

  • New membuat coroutine baru yang awalnya dijeda dan menyiapkan eksekusi fungsi f
    • Coroutine baru itu adalah goroutine, tetapi tidak berjalan sendiri
    • Ia hanya berjalan saat goroutine lain memanggil resume atau cancel dan menunggu
  • resume(in) menghentikan goroutine pemanggil dan beralih ke coroutine baru
    • Pemanggilan pertama memulai f(in, yield)
    • resume akan terblokir sampai f memanggil yield(out) atau mengembalikan out
    • Jika yield dipanggil, resume mengembalikan out, true
    • Jika f mengembalikan hasil, resume mengembalikan out, false
    • resume(in) berikutnya membuat yield yang sempat terblokir mengembalikan in
  • cancel menghentikan eksekusi f dan mengakhiri coroutine
    • Jika resume belum pernah dipanggil, f tidak akan dijalankan
    • Jika tidak, yield yang sedang terblokir akan panic dengan error yang memenuhi ErrCanceled
  • Jika f menghasilkan panic yang tidak berhasil di-recover, panic itu akan berpindah ke goroutine yang sedang menunggu di resume atau cancel lalu mem-panic ulang dengan nilai yang sama
    • Namun, cancel tidak akan mem-panic ulang panic pembatalan yang dipicunya sendiri
  • Jika f mengembalikan hasil atau panic, coroutine itu tidak ada lagi
    • Pemanggilan resume setelahnya akan mengembalikan zero value dan false
    • Pemanggilan cancel setelahnya hanya langsung kembali
  • resume, cancel, dan yield dapat diteruskan ke goroutine lain untuk digunakan
    • Akibatnya, goroutine mana yang menjadi “coroutine” bisa berubah secara dinamis
  • New membuat goroutine baru, tetapi mempertahankan invariannya bahwa selalu ada satu goroutine yang terblokir di resume, cancel, yield, atau pada keadaan menunggu awal
    • Invarian ini dipertahankan sampai f mengembalikan hasil
    • Akibatnya, coro.New menciptakan konkurensi baru, tetapi tidak menciptakan paralelisme baru
  • Signature akhirnya adalah sebagai berikut
func New[In, Out any](f func(in In, yield func(Out) In) Out) (resume func(In) (Out, bool), cancel func())

efisiensi

  • Seharusnya coroutine dapat didefinisikan dengan implementasi Go murni, tetapi penggunaan nyata memerlukan implementasi runtime yang dioptimalkan
  • Pada MacBook Pro 2019, coro.New berbasis channel memerlukan sekitar 190ns per perpindahan untuk pertukaran nilai bolak-balik
    • Pada coro.Pull, biayanya sekitar 380ns per nilai
  • coro.Pull bukan cara standar menggunakan iterator
    • Cara standarnya adalah memanggil iterator secara langsung, dan dalam kasus ini tidak ada overhead coroutine
    • coro.Pull diperlukan saat nilai perlu diproses secara bertahap, bukan dalam satu loop for
  • Upaya optimasi pertama adalah compiler menandai pasangan send-receive dan runtime meninggalkan petunjuk agar pasangan itu digabung menjadi satu operasi
    • Runtime channel dapat melewati scheduler dan langsung melompat ke coroutine lain
    • Biayanya sekitar 118ns per perpindahan, dan sekitar 236ns per pulled value
    • 38% lebih cepat daripada implementasi channel asli
  • Implementasi kedua sepenuhnya menghindari channel dan menambahkan perpindahan coroutine langsung ke runtime
    • Perpindahan coroutine berkurang menjadi tiga atomic compare-and-swap
    • Satu digunakan untuk struktur data coroutine, satu untuk scheduler status coroutine yang diblokir, dan satu untuk scheduler status coroutine yang di-resume
    • Biayanya sekitar 20ns per perpindahan, dan sekitar 40ns per pulled value
    • Sekitar 10 kali lebih cepat daripada implementasi channel asli
  • Biaya 40ns per nilai dianggap cukup kecil secara absolut sehingga tidak menjadi bottleneck pada kode yang membutuhkan coro.Pull

1 komentar

 
GN⁺ 2023-07-18
Komentar Hacker News
  • Sepertinya banyak orang melewatkan poin utamanya di sini. Memang benar bahwa library coroutine adalah cara yang lebih buruk dan merepotkan untuk menangani konkurensi dibandingkan kata kunci go
    Kegunaan nyata yang membawa kompleksitas ini adalah iterator fungsi, yaitu memungkinkan penggunaan range pada fungsi bertipe func() (T, bool). Ini sudah lama dibahas di komunitas Go, dan maknanya mungkin intuitif bagi sebagian besar programmer Go
    Tulisan ini membahas masalah berikutnya: jika iterator fungsi ditambahkan ke bahasa, bagaimana cara menulis iterator yang akan dipakai dalam loop for. Dimulai dari fakta bahwa iterator push sering kali lebih mudah ditulis, lalu diperluas menjadi adapter push-pull, dan adapter itu dibangun di atas coroutine
    Jika semuanya masuk, memakai coroutine untuk tujuan selain iterasi tampaknya akan menjadi praktik buruk, seperti memakai channel/goroutine di tempat yang sebenarnya cukup dengan mutex

    • Perlu juga disebutkan bahwa pada kasus penggunaan tertentu, coroutine jauh lebih efisien daripada goroutine penuh. Sebab saat beralih dengan coroutine, tidak diperlukan context switch atau penjadwalan ulang
      Jika dua tugas bekerja sama secara logis dan sinkron, misalnya iterator, menjalankan semuanya pada CPU yang sama jauh lebih efisien. Kernel tidak perlu menjadwalkan ulang apa pun atau mematikan dan membangunkan core CPU, dan data juga tetap sesuai di cache CPU, sehingga latensi cache dan hit rate menjadi lebih baik
      Pada goroutine, hal itu bisa saja terjadi secara kebetulan, tetapi tidak dijamin, dan setidaknya ada biaya melewati scheduler goroutine milik runtime Go. Memang cepat, tetapi tidak secepat menjalankan konteks kode lain di dalam goroutine yang sama
      Dengan coroutine, kita bisa tahu bahwa tugas A beralih langsung ke tugas B, sehingga perilaku penjadwalan lebih dapat diprediksi. Di bagian akhir tulisan, Russ menunjukkan bahwa implementasi coroutine runtime yang dioptimalkan 10 kali lebih cepat daripada tiruan yang dibuat dengan goroutine
      Di Google, ada patch kernel internal yang mengimplementasikan cooperative multithreading seperti ini, dan secara internal disebut fibers. Itu ada demi latensi yang lebih baik dan penjadwalan yang dapat diprediksi. Ada juga presentasi Paul Turner di LPC sekitar 10 tahun lalu yang menjelaskan motivasinya: https://www.youtube.com/watch?v=KXuZi9aeGTw
    • Saya tidak mengerti apa masalahnya kalau ditulis sebagai for { next := getNext(); ... }. Saya penasaran apa keunggulan menulisnya sebagai for next := range getNext { ... }
    • Coroutine di Go sepertinya bisa membuat Go digunakan sebagai bahasa host untuk mendefinisikan simulasi elemen diskret dengan rapi. Saat ini cara menyerahkan kendali ke aktor terasa canggung
    • Saya tidak mengerti kenapa tidak cukup memakai range atau switch pada channel, lalu menjalankan satu goroutine yang mendorong nilai ke channel. Saya masih belum yakin kenapa coroutine diperlukan
    • Tetap saja, pada akhirnya ini terasa seperti penambahan ala kitchen sink
      Kali ini bukan pemikiran yang hati-hati dan solusi 80/20 yang bagus, melainkan terlihat seperti, “Untuk melakukan ini dengan benar kita butuh coroutine, ya sudah masukkan saja”
      Saat menambahkan generics, mereka benar-benar berpikir lama dan mendalam, lalu menghasilkan kompromi inovatif dengan keseimbangan yang sangat bagus
      Di sini saya berharap pendekatan seperti “menambahkan satu fitur ke goroutine agar bisa dikendalikan pada situasi tertentu”. Itu akan terlihat lebih baik daripada “untuk masalah ini kita maju habis-habisan seperti Rust dan langsung menambahkannya”
  • Saya sudah memakai Go secara profesional selama bertahun-tahun, tetapi saya tidak ingin Go berubah menjadi seperti Twisted / Tornado / framework lain di Python
    Kata kunci go cukup baik mencegah masalah function coloring yang menyakitkan
    Dalam konteks performa tinggi, kadang kita ingin melakukan hal seperti sharding data per core CPU, tetapi proposal ini tidak menggaruk rasa gatal semacam itu

    • Coroutine dan goroutine mengisi ceruk yang berbeda. Goroutine sudah mengisi area yang dulu ditempati hal-hal seperti Twisted. Di sini tidak ada upaya membawa sesuatu yang mirip async/await ke Go
      Coroutine akan mengisi area lain yang lebih mirip generator di Python. Pada tempat yang perlu merangkai pipeline komponen yang dapat dikomposisi, ini sering kali bisa sangat mengurangi penggunaan memori dan kompleksitas kode. Desainnya tampak sepenuhnya sinkron
    • Tidak ada satu pun bagian tulisan yang mengusulkan sesuatu yang bisa disebut masalah function coloring
    • Jika Anda harus menulis goroutine dan memakai Context, itu secara harfiah adalah function coloring
    • “Masalah” pada framework async/threading lain mungkin adalah bahwa threading dibangun di atas iterator/coroutine. Dalam kasus ini, keduanya ortogonal, jadi sepertinya tidak seburuk yang dibayangkan
      Tentu saja, hampir pasti akan ada developer pintar di suatu tempat di dunia yang membuat sesuatu yang bodoh dengan ini, lalu itu menjadi sangat populer. Perkiraan saya adalah mengabstraksikan goroutine dan coroutine menjadi satu
    • Saya penasaran apakah ada yang bisa berbagi petunjuk tentang pola manajemen channel atau framework yang lebih baik
      Biasanya kode terkait goroutine terasa berantakan karena channel, dan saya tidak tahu bagaimana membuatnya lebih “rapi”. Jika ada petunjuk pola yang terbukti mudah dipelihara, saya akan sangat berterima kasih
  • Sistem multitasking memberi kita proses
    Namun itu terlalu berat
    Maka muncullah thread, yaitu proses yang berbagi ruang alamat, tabel file, dan beberapa hal lain. Scheduler bisa beralih antar-thread lebih mudah daripada antar-proses, dan berbagi data antar-thread tidak memerlukan serialisasi
    Namun itu juga terlalu berat
    Maka muncullah thread ruang pengguna. Ini adalah thread eksekusi logis yang dijalankan sepenuhnya oleh runtime di ruang pengguna. Runtime memasang hook penjadwalan pada semua fungsi input/output di pustaka standar, atau melakukan preemption terhadap thread logis dengan API sistem seperti sinyal Unix. Tidak perlu context switch tingkat sistem, dan bisa dibuat sangat kecil
    Namun itu juga terlalu berat
    Maka muncullah coroutine. Ini memungkinkan programmer mendefinisikan “thread” logis yang saling berinteraksi secara kooperatif. Tidak mengasumsikan adanya scheduler. Programmer menulis event loop sendiri, atau memanggil event loop pustaka dari thread logis yang “sebenarnya”
    Saya penasaran apa yang akan datang berikutnya. Dari sudut pandang [communicating sequential processes][1], coroutine kooperatif mungkin merupakan tahap terendah yang bisa dicapai
    [1]: https://www.cs.cmu.edu/~crary/819-f09/Hoare78.pdf

    • Coroutine juga tidak menyelamatkan programmer dari kengerian harus menjelaskan setiap detail yang mungkin kepada komputer. Terutama harus menjelaskan agar eksekusi tidak berlangsung selamanya ketika input sedikit saja berbeda dari yang dipikirkan programmer
    • Agak mengejutkan bahwa tidak banyak bahasa yang secara otomatis memparalelkan kode sejauh mungkin, tetapi hanya sampai batas ketika performa terukur menunjukkan keuntungan
      Diperlukan semantik yang mendukung concurrency. Misalnya, traversal seharusnya secara default tidak memiliki urutan, dan ini juga bergantung pada analisis alur seluruh program, tetapi saya tidak melihat hambatan prinsipiel
      Bukankah Microsoft pernah meneliti bahasa seperti itu beberapa tahun lalu? Ada juga ParaSail yang dibuat oleh seseorang dari komunitas Ada. Saya penasaran apa yang terjadi dengan proyek-proyek seperti ini, apakah tidak ada yang memakainya
    • Menurut saya tidak ada tempat lagi untuk turun lebih jauh. Sebaliknya, mungkin ada ruang untuk naik ke arah komputasi terdistribusi. Kalau ingatan saya benar, di salah satu versi alfa awal Go, channel juga bekerja antar-mesin
    • Langkah berikutnya bisa berbentuk sesuatu seperti “proses koleksi item kerja ini dengan cara apa pun yang kamu inginkan”. Jika dilihat sambil menyipitkan mata, semua eksekusi serentak bisa dipandang sebagai urutan pekerjaan, dan pekerjaan itu sendiri juga bisa berupa koleksi. Ini tetap berlaku meskipun pekerjaannya hanya satu atau dua
  • Saya kira inti dari green thread adalah mendapatkan penjadwalan kooperatif yang baik tanpa harus memakai keyword seperti yield di Python
    Saya memandang keputusan desain Go untuk menyisipkan titik resume pada titik pemanggilan dan lokasi tertentu sebagai kompromi yang sangat baik
    Semakin lama makin banyak kontrol yang dekat dengan hardware diekspos. Pada titik tertentu, saya tidak tahu apakah ini hanya membuat ulang Zig. Berikutnya apakah garbage collector opsional?

    • Di sini yield dan resume bukan keyword, melainkan variabel biasa. Itu hanya referensi closure biasa yang diberi nama demikian untuk tujuan edukasi
      Hal yang tidak biasa adalah bagian membuat dan memakai callback pembatalan. Saya belum banyak memakai Go, jadi saya tidak tahu apakah ini optimisasi performa untuk lebih cepat mengumpulkan state iterator yang sudah ditinggalkan, atau diperlukan karena Go tidak melakukan garbage collection pada goroutine yang menunggu di sebuah channel ketika goroutine itu memegang satu-satunya referensi ke channel tersebut
      Di Lua, hal seperti ini tidak diperlukan. Coroutine/thread juga di-garbage collect seperti objek lain, jadi ketika semua referensi hilang, ia akan dikumpulkan meskipun operasi terakhirnya adalah yield, bukan return dari fungsi entry
    • Garbage collector Go bersifat opsional. Jika mengatur variabel lingkungan GOGC=off, garbage collection bisa dimatikan
      Detail lebih lanjut tentang GOGC: https://dave.cheney.net/tag/gogc
  • Untuk coroutine, saya rasa saya lebih memilihnya masuk sebagai dukungan bahasa daripada pustaka
    Saya membayangkan bentuk seperti x := co func(){ var z int; for { z++; yield z } }, atau sesuatu yang setara
    Keren bahwa ini bisa dilakukan hanya dengan Go murni, dan saya juga memahami daya tarik menyediakannya sebagai paket pustaka standar dengan runtime yang dioptimalkan, alih-alih memperumit spesifikasi bahasa. Pada akhirnya, jika bisa dilakukan dengan Go murni, implementasi lain juga bisa melakukan bootstrap dengan cepat
    Sebagai orang yang memakai Go setiap hari di $work, saya akan menyambut salah satu dari keduanya, tetapi lebih memilihnya terintegrasi ke dalam bahasa. Primitif concurrency Go selalu menjadi kekuatannya, jadi tinggal dorong ke arah itu

    • Coroutine memerlukan dukungan bahasa. Apa yang akan dilakukan jika stack habis di salah satu coroutine?
      Dengan solusi pustaka, coroutine harus dimatikan atau program harus dimatikan. Jika ingin stack bertambah secara transparan, hanya kode yang dihasilkan yang bisa melakukannya, karena perlu mengawasi penggunaan stack dan memperbesarnya saat dibutuhkan. Sejauh yang saya tahu, goroutine memiliki hal seperti itu
      Mungkin solusi pustaka juga bisa menaruh guard page di ujung stack. Ketika mencapainya, handler error bisa mencoba memperluas stack. Namun jika pointer ke variabel stack sudah disimpan, kemungkinan ini tidak akan bekerja
    • Saya lebih memilih menambahkan parameter yield ke fungsi agar berinteraksi lebih baik dengan sistem tipe saat ini
      Caranya, fungsi yang ingin me-yield nilai harus memiliki apa yang disebut parameter yield, dan hanya boleh me-yield di dalam fungsi dengan signature yield yang sama atau di dalam fungsi tanpa signature semacam itu
      Contohnya bisa berubah menjadi bentuk seperti x := func(:z int) { for { z++; :- z } }. : menambahkan signature yield, dan :- me-yield nilai
      Fungsi yang hanya me-yield nilai X cukup memiliki : X atau : name X. Untuk menerima nilai bertipe Y saat resume, signaturenya berubah menjadi :[Y] X atau :[Y] name X
      Penerimaannya harus lemah. Di tempat yang mengharapkan fungsi yang di-resume dengan Y dan me-yield X, fungsi yang hanya me-yield X tanpa resume juga harus diterima
      Jika fungsi-fungsi khusus dalam paket co menyediakan fitur resume dan New, gaya Go bisa tetap dipertahankan. Sintaks range bisa diperluas dengan cara meneruskan nilai resume menggunakan -:, dan jika tidak meneruskan nilai, resume dilakukan dengan zero value default
    • Ini benar-benar mengerikan. Sama sekali tidak intuitif, dan juga tidak cocok dengan salah satu kekuatan Go. Untuk menulis atau membaca kode seperti ini, orang harus mempelajari semantik coroutine Go secara terpisah
  • Saya kurang suka. Dari contoh-contohnya, rasanya ini membuat bahasa jadi jauh lebih sulit dibaca dan diikuti. Tentu saja bisa jadi karena kepala dan bias saya sendiri
    Selain itu, sepertinya ini juga tidak memungkinkan sesuatu yang tidak bisa dilakukan dengan channel blocking atau state yang ada sekarang

    • Tepat seperti yang dikatakan. Orang-orang yang membela ini sepertinya hanya bisa melihat satu langkah di depan
    • Saya tidak tahu perubahan bahasa apa yang dimaksud. Ini hanya proposal untuk menormalkan dan mengefisienkan hal yang sudah dilakukan orang dengan “state”
      Saya pernah memakai iterator yang mirip dengan yang ada di tulisan ini untuk menghindari alokasi di jalur kode inti. Dengan cara ini, kode seperti itu akan jauh tidak canggung. Apalagi jika digabung dengan perubahan bahasa iterator range yang akan segera masuk
    • Memiliki puluhan implementasi iterator yang tidak saling kompatibel seperti di pustaka standar saat ini juga bukan berarti lebih mudah
      Channel lambat tanpa alasan khusus ketika tidak membungkus operasi blocking
  • Membaca komentar-komentarnya membuat saya getir
    Banyak orang menganggap coroutine dan green thread hampir sama, tetapi keduanya punya kelebihan dan kekurangan
    Sedih melihat ketiadaan iterator di komunitas Go bisa dianggap dapat diterima. Dengan dalih kesederhanaan, mereka tampaknya sengaja menolak fitur apa pun yang bisa membuat bahasa sedikit lebih kompleks. Meski begitu, setidaknya mereka sudah menarik kembali sikap mereka soal generics
    Ini kembali membuat saya merasa bahwa Go memang bukan bahasa untuk saya

    • Saya memakai Go setiap hari, tetapi jujur saja generics tidak banyak mengubah kode saya
      Sekarang saya memakainya sedikit, karena kenyamanan sintaks di titik pemanggilan agak lebih baik. Di titik definisi, seperti yang diduga, masih tetap jelek, hanya saja sintaks Go termasuk hampir yang terbaik di antara yang pernah saya lihat di bahasa lain
      Pada akhirnya, menurut saya cukup banyak kesederhanaan yang dikorbankan demi meredam keluhan “tidak ada generics”. Itu bukan pertukaran yang bagus
    • Jangan menyamakan HN dengan “komunitas Go”. Penulis tulisan ini adalah pemimpin tim Go
      Apakah paket coro yang diusulkan dalam tulisan ini akan ditambahkan apa adanya? Mungkin saja, tetapi kemungkinan tidak persis seperti itu. Apakah sesuatu yang mirip akan ditambahkan? Kalau bisa bertaruh, saya akan bilang ya
      Berapa lama? Minimal 1 tahun, kira-kira rilis Go 1.23 pada Agustus 2024. Bisa juga sedikit lebih lama. Saya rasa sulit kalau jauh lebih cepat dari itu
    • Mereka tidak menarik kembali sikap soal generics
      Generics diterima komunitas karena sepenuhnya kompatibel ke belakang dengan kode yang ada, dan kalau tidak perlu bisa diabaikan dengan aman
      Tidak mengejutkan, sebagian besar kode Go masih melakukan itu. Selain beberapa jenis “koleksi”, kegunaan praktis generics tidak terlalu mudah ditemukan. Sejak awal, kebanyakan kode tidak perlu melihat lebih dari satu tipe, dan kalau lebih dari dua tipe itu sudah tergolong jarang
      Justru penambahan generics menunjukkan kepada komunitas yang lebih luas betapa fitur-fitur yang dipuja dan dituntut orang sebagai “wajib” sebenarnya jauh kurang diperlukan dalam bahasa bagus yang diterima luas
    • Generics tidak mengubah 20% pun dari codebase saya, dan sebagian dari 20% itu adalah pustaka. Entah ini kebiasaan Go atau kebiasaan C, tetapi generics tampak seperti satu lagi pustaka tempat seorang programmer di suatu tempat sudah menyelesaikan suatu masalah
    • Saya kira komunitas Go akan menyambut antarmuka iterasi yang terpadu
  • Sepertinya bagus bahwa baru sekarang mereka mulai memperhatikan bahasa pemrograman seperti CLU
    Di sisi lain, berdasarkan pengalaman memakai coroutine .NET dan C++, serta Active Object di Symbian C++ dan Active Oberon, saya tidak yakin apakah ini benar-benar layak ditambahkan ke Go
    Seperti yang juga diakui tim .NET di BUILD tahun ini, kalau bisa memutar balik waktu, akan lebih baik jika runtime menanganinya dengan gaya Go. Sebab banyak developer masih kesulitan memahami async/await

  • Saya tidak tahu apakah ini benar-benar diperlukan. Sebagian besar kasus di Go sudah bisa ditangani dengan goroutine, dan semantik yield/resume cukup dengan 2 channel blocking
    Ini terlihat seperti menambah kompleksitas demi kompleksitas, dan saya juga tidak yakin apakah benar-benar menambahkan kemampuan baru yang sebelumnya tidak ada di Go

    • Goroutine dan channel menambahkan overhead yang sangat besar. Memakainya sebagai iterator pada dasarnya tidak masuk akal
    • Hal ini dibahas dalam tulisan tersebut
  • Sebagai pembanding, dalam presentasi baru-baru ini saya mendemokan menjalankan 1 juta thread Elixir (BEAM VM), mengirim pesan "Hello!" ke semuanya, lalu tiap thread menunggu selama waktu acak antara 0–2 detik dan mengirim balik "Process received message !"
    Pada saat yang sama, saya membuka Erlang observer di sampingnya untuk mengamati konsumsi CPU dan memori, serta seberapa cepat pulih setelah garbage collection
    Bottleneck terbesar di sini adalah kemampuan terminal untuk mengejar output, tetapi observer tampaknya cukup akurat mencerminkan keadaan sebenarnya
    https://www.youtube.com/watch?v=yxyYKnashR0
    Kode yang digunakan: https://gist.github.com/pmarreck/4cc8f2f55a561ebce2012085a3a...
    Fitur seperti ini sudah tertanam di Erlang sejak 1980-an, dan karenanya juga di Elixir. Anda mungkin sering mendengar tentang model aktor atau implementasi Erlang yang “legendaris”, tetapi saya tidak tahu berapa banyak orang yang benar-benar pernah melihatnya berjalan sambil menampilkan alat monitoring
    Akan bagus jika Go menyediakan dukungan tingkat bahasa seperti ini, tetapi implementasi thread BEAM VM sangat efisien sumber daya, baik dalam pembuatan maupun konsumsi saat runtime, dan ditambah lagi kemudahan konkurensi yang datang dari hanya mengizinkan nilai immutable, sehingga tampaknya akan sangat sulit untuk dikejar