- 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
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
goKegunaan nyata yang membawa kompleksitas ini adalah iterator fungsi, yaitu memungkinkan penggunaan
rangepada fungsi bertipefunc() (T, bool). Ini sudah lama dibahas di komunitas Go, dan maknanya mungkin intuitif bagi sebagian besar programmer GoTulisan 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 coroutineJika semuanya masuk, memakai coroutine untuk tujuan selain iterasi tampaknya akan menjadi praktik buruk, seperti memakai channel/goroutine di tempat yang sebenarnya cukup dengan mutex
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
for { next := getNext(); ... }. Saya penasaran apa keunggulan menulisnya sebagaifor next := range getNext { ... }rangeatauswitchpada channel, lalu menjalankan satu goroutine yang mendorong nilai ke channel. Saya masih belum yakin kenapa coroutine diperlukanKali 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
gocukup baik mencegah masalah function coloring yang menyakitkanDalam konteks performa tinggi, kadang kita ingin melakukan hal seperti sharding data per core CPU, tetapi proposal ini tidak menggaruk rasa gatal semacam itu
async/awaitke GoCoroutine 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
Context, itu secara harfiah adalah function coloringTentu 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
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
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
Saya kira inti dari green thread adalah mendapatkan penjadwalan kooperatif yang baik tanpa harus memakai keyword seperti
yielddi PythonSaya memandang keputusan desain Go untuk
menyisipkan titik resume pada titik pemanggilan dan lokasi tertentusebagai kompromi yang sangat baikSemakin 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?
yielddanresumebukan keyword, melainkan variabel biasa. Itu hanya referensi closure biasa yang diberi nama demikian untuk tujuan edukasiHal 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 entryGOGC=off, garbage collection bisa dimatikanDetail 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 setaraKeren 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 ituDengan 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
yieldke fungsi agar berinteraksi lebih baik dengan sistem tipe saat iniCaranya, 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 ituContohnya bisa berubah menjadi bentuk seperti
x := func(:z int) { for { z++; :- z } }.:menambahkan signature yield, dan:-me-yield nilaiFungsi yang hanya me-yield nilai
Xcukup memiliki: Xatau: name X. Untuk menerima nilai bertipeYsaat resume, signaturenya berubah menjadi:[Y] Xatau:[Y] name XPenerimaannya harus lemah. Di tempat yang mengharapkan fungsi yang di-resume dengan
Ydan me-yieldX, fungsi yang hanya me-yieldXtanpa resume juga harus diterimaJika fungsi-fungsi khusus dalam paket
comenyediakan fiturresumedanNew, gaya Go bisa tetap dipertahankan. Sintaksrangebisa diperluas dengan cara meneruskan nilai resume menggunakan-:, dan jika tidak meneruskan nilai, resume dilakukan dengan zero value defaultSaya 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
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
rangeyang akan segera masukChannel 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
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
Apakah paket
coroyang 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 yaBerapa 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
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
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/resumecukup dengan 2 channel blockingIni terlihat seperti menambah kompleksitas demi kompleksitas, dan saya juga tidak yakin apakah benar-benar menambahkan kemampuan baru yang sebelumnya tidak ada di Go
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