I/O Asinkron Baru di Zig
(kristoff.it)- Dengan diperkenalkannya antarmuka I/O asinkron baru di Zig, pemanggil kini dapat langsung memilih dan menyuntikkan cara implementasi I/O
- Antarmuka Io yang dirancang ulang mendukung asinkronitas dan paralelisme secara bersamaan, dengan fokus pada penggunaan ulang kode dan optimisasi
- Akan tersedia berbagai implementasi pustaka standar seperti Blocking I/O, event loop, thread pool, green thread, dan coroutine stackless
- Melalui API baru, dimungkinkan pembatalan future, pengelolaan sumber daya, buffering, serta perilaku input/output yang lebih terperinci
- Masalah function coloring yang ada sebelumnya dapat diatasi, sehingga satu pustaka bisa dioptimalkan untuk operasi sinkron maupun asinkron
Ikhtisar
Zig baru-baru ini merancang antarmuka I/O asinkron baru, sebagai bagian dari perkembangan yang menitikberatkan pada fleksibilitas pekerjaan I/O dan dukungan paralelisme. Perubahan ini memisahkan paradigma async/await yang ada sebelumnya, sehingga penulis program dapat mengadopsi strategi I/O yang jauh lebih beragam.
Antarmuka I/O baru
Sebelumnya, objek-objek terkait I/O dibuat dan digunakan langsung di dalam kode, tetapi sekarang antarmuka Io disuntikkan oleh pemanggil.
- Pendekatan ini mirip dengan pola Allocator, di mana sisi pemanggil memilih dan menyuntikkan implementasi I/O yang konkret
- Strategi I/O juga dapat diterapkan secara konsisten pada kode paket eksternal
Perubahan utama
- Antarmuka Io kini juga menangani operasi konkurensi
- Jika kode mengekspresikan konkurensi dengan benar, maka tergantung implementasi Io, paralelisme juga bisa disediakan
Contoh kode
- Dibandingkan dua jenis kode: kode tanpa konkurensi (serial), dan kode yang mengekspresikan kemungkinan paralel melalui
io.asyncdanawait- Kode serial: menyimpan ke dua file secara berurutan, sehingga tidak dapat memanfaatkan peluang paralelisme
- Kode paralel: menyimpan file dengan memanfaatkan future, sehingga bekerja lebih efisien dalam event loop asinkron
Kombinasi await dan try
- Saat
awaitdantrydigunakan bersama, ada masalah di mana jika terjadi error pada satu future, sumber daya dari future lain mungkin tidak sempat dilepas - Dengan
deferdanfuture.cancel, pembatalan dan pembersihan dapat dinyatakan dengan jelas dan tepat
API Future.cancel
Future.cancel()danFuture.await()bersifat idempotent (dipanggil berkali-kali pun tidak menimbulkan efek samping)- Jika
canceldipanggil pada future yang sudah selesai, hanya sumber dayanya yang dibebaskan; untuk pekerjaan yang belum selesai, akan dikembalikanerror.Canceled
Implementasi I/O pustaka standar
Antarmuka Io adalah antarmuka berbasis polimorfisme runtime, sehingga bisa diimplementasikan langsung atau menggunakan implementasi dari paket pihak ketiga. Pustaka standar Zig berencana menyediakan berbagai jenis implementasi I/O.
- Blocking I/O: cukup menggunakan input/output blocking gaya C yang sudah ada, tanpa overhead tambahan
- Thread pool: mendistribusikan Blocking I/O ke thread pool OS, menghadirkan sebagian paralelisme. Pada kasus seperti klien jaringan, masih diperlukan optimisasi
- Green thread: memanfaatkan system call asinkron seperti
io_uringdi Linux, sehingga banyak thread hijau (ringan) dapat ditangani di atas thread OS. Memerlukan dukungan platform (prioritas awal pada Linux x86_64) - Coroutine stackless: coroutine berbasis state machine tanpa kebutuhan stack eksplisit. Ditujukan untuk kompatibilitas dengan sebagian platform seperti WASM. Memerlukan pengenalan kembali protokol konvensi pada kompiler Zig
Tujuan desain
Penggunaan ulang kode
Masalah terbesar dalam I/O asinkron adalah penggunaan ulang kode; di bahasa lain, fungsi blocking dan async biasanya terpisah sehingga kode ikut terbelah. Pendekatan Zig:
- Satu pustaka dapat mendukung mode sinkron dan asinkron secara efektif
- async/await menghilangkan fenomena "function coloring", dan melalui sistem Io, kode juga tidak bergantung pada model eksekusi tertentu saat runtime
Kesimpulannya, masalah function coloring diselesaikan sepenuhnya
Optimisasi
- Antarmuka Io baru diimplementasikan sebagai non-generic, dengan pemanggilan virtual berbasis vtable
- Pemanggilan virtual mengurangi pembengkakan kode, tetapi menambah sedikit overhead saat eksekusi. Pada build optimisasi, jika hanya ada satu implementasi Io, dimungkinkan de-virtualization (penghapusan pemanggilan virtual)
- Jika menggunakan beberapa implementasi Io, pemanggilan virtual tetap dipertahankan (untuk mencegah duplikasi kode)
Strategi buffering
- Sebelumnya, tiap implementasi (
reader/writer) menangani buffering sendiri, tetapi sekarang buffering dilakukan di level antarmuka Reader dan Writer - Selain flush buffer, jalur pemanggilan virtual tidak dilalui, sehingga lebih mudah dioptimalkan
Operasi I/O semantik
Antarmuka Writer menyediakan dua primitive baru untuk operasi optimisasi tertentu
- sendFile: terinspirasi dari POSIX
sendfile, memindahkan data antar file descriptor di dalam kernel. Menekan penyalinan memori seminimal mungkin - drain: mendukung vectorized write + splatting. Beberapa segmen data dapat dikirim sekaligus dan dapat diterjemahkan menjadi system call
writev. Dengan parametersplat, elemen terakhir bisa diulang untuk digunakan, misalnya, pada stream seperti kompresi
Peta jalan
Sebagian dari perubahan ini mulai diperkenalkan di Zig 0.15.0, tetapi karena memerlukan perombakan besar pada pustaka, penerapan penuh harus menunggu rilis berikutnya. Modul utama seperti SSL/TLS, HTTP server/client, dan lainnya juga dijadwalkan untuk didesain ulang dengan sistem Io baru.
FAQ
Q: Zig adalah bahasa low-level, jadi kenapa async penting?
- Zig berorientasi pada ketangguhan, optimisasi, dan penggunaan ulang
- Dengan menstandarkan input/output non-blocking, pustaka lain maupun kode pihak ketiga juga dapat disesuaikan dengan strategi I/O keseluruhan dan memperoleh penggunaan ulang yang lebih baik
Q: Apakah penulis paket sekarang harus menggunakan async di semua kode?
- Tidak
- Tidak semua kode perlu mengekspresikan konkurensi
- Kode sekuensial biasa pun tetap bekerja sesuai strategi I/O yang dipilih pengguna
Q: Apakah model eksekusi apa pun akan selalu berjalan normal asal dipasang sebagai plugin?
- Dalam kebanyakan kasus, ya
- Namun, jika ada kesalahan pemrograman pada kode (misalnya tidak memenuhi persyaratan pekerjaan konkuren), maka program tidak akan berjalan dengan benar
Disebutkan pula, lewat contoh eksekusi, perbedaan antara asinkronitas dan paralelisme serta perlunya perancangan alur kerja yang benar.
Kesimpulan
Dengan diperkenalkannya antarmuka Io baru, Zig secara signifikan meningkatkan fleksibilitas pemilihan strategi input/output, penggunaan ulang kode, dan potensi optimisasi. Dengan demikian, tanpa terikat pada batasan penulisan fungsi berbasis sinkron/asinkron, pengembang dapat mengekspresikan struktur konkurensi dan paralelisme dengan lebih jelas serta merespons beragam platform dan model eksekusi secara efektif.
1 komentar
Komentar Hacker News
Saya ingin menekankan poin ini lagi. Artikel itu bahkan menyebut Zig sepenuhnya menyelesaikan masalah function coloring, tetapi saya tidak setuju. Jika kita memikirkan kembali 5 aturan dari tulisan terkenal "What color is your function?", di Zig memang tidak ada pembedaan warna seperti async/sync/red/blue, tetapi pada akhirnya tetap hanya ada dua kasus: fungsi IO dan fungsi non-IO. Memang masalah perbedaan cara pemanggilan fungsi berdasarkan warna telah diselesaikan secara teknis, tetapi fungsi yang membutuhkan IO tetap harus menerima IO sebagai argumen, sementara fungsi yang tidak membutuhkannya tidak menerimanya. Jadi rasanya esensinya tidak berubah. Fungsi IO hanya bisa dipanggil dari fungsi IO, dan ini pun tetap belum keluar dari masalah coloring. Tentu executor baru juga bisa diteruskan, tetapi saya ragu itu benar-benar yang diinginkan. Di Rust pun hal serupa bisa dilakukan. Soal pemanggilan fungsi berwarna yang merepotkan juga tetap sama. Fakta bahwa beberapa fungsi library inti berwarna juga tidak berlaku baik di Zig maupun Rust. Esensi masalah coloring adalah bahwa fungsi yang memerlukan konteks, yaitu async executor atau auth, allocator, dan sebagainya, harus selalu diberi konteks itu saat dipanggil. Sulit mengatakan Zig benar-benar menyelesaikan bagian ini. Meski begitu, abstraksi Zig sangat bagus, dan Rust memang agak kurang di area ini. Tetapi masalah function coloring itu sendiri masih tetap ada
Perbedaan inti dengan async function coloring yang lazim adalah bahwa
Iodi Zig bukan sekadar nilai khusus untuk pemrosesan asinkron, melainkan nilai yang memang diperlukan untuk semua IO seperti membaca file, sleep, mengambil waktu, dan sebagainya.Iobukan sifat dari sebuah fungsi, tetapi nilai biasa yang bisa ditempatkan di mana saja. Dalam praktiknya, justru karena karakteristik ini, masalah coloring terlihat seperti terselesaikan. Di sebagian besar codebase, IO sudah ada di suatu scope, jadi hanya fungsi komputasi yang benar-benar murni yang tidak memerlukan IO. Jika sebuah fungsi tiba-tiba membutuhkan IO, dalam kebanyakan kasus ia bisa langsung mengambil dan memakainya darimy_thing.io. Tidak seperti Rust, tidak perlu meneruskan Allocator ke semua fungsi, jadi tidak terlalu merepotkan. Artinya, jika jalur kode berubah dan perlu melakukan IO, kita tidak perlu menyebarkan perubahan ke setiap fungsi, cukup langsung memakainya. Secara prinsip saya setuju bahwa function coloring masih ada, tetapi pada praktiknya hampir semua fungsi menjadi async-colored, sehingga masalah nyatanya nyaris tidak ada. Faktanya, pengembang Zig menganggap meneruskan Allocator secara eksplisit tidak menimbulkan kerepotan function coloring. Saya kiraIojuga tidak akan jadi masalah besarMenurut saya ada poin penting yang belum disebut. Saat memakai library Rust, kita harus menyesuaikan syarat seperti async/await, tokio, send+sync, dan jika API-nya sync maka praktis tak berguna di aplikasi async. Sebaliknya, cara Zig meneruskan IO menyelesaikan masalah ini secara mendasar. Karena itu, kita tidak perlu bersusah payah memaksa procedural macro atau multi-versioning, dan sebenarnya pendekatan semacam itu juga tidak benar-benar menyelesaikan masalah multi-versi library dengan baik. Ada berbagai diskusi tentang masalah pencampuran async/sync di Rust, dan dijelaskan juga di tautan berikut https://nullderef.com/blog/rust-async-sync/. Semoga ke depan Zig juga bisa menangani cooperative scheduling, async berperforma tinggi, dan async thread-per-core dengan baik
Saya bukan ahli teori kategori, tetapi pada akhirnya jika menempuh jalur pengelolaan konteks seperti ini, kita akan sampai pada monad IO. Konteks ini bisa bersifat implisit, tetapi jika ingin benar-benar dibantu compiler, ia harus muncul sebagai entitas nyata di dalam sistem. Dan meskipun ambisi bahasa system programming berkali-kali terkubur di kuburan Async atau coroutine, Andrew tampaknya menemukan kembali monad IO dengan caranya sendiri dan mengimplementasikannya dengan benar, yang menurut saya memberi harapan bagi generasi ini. Fungsi dunia nyata memang punya warna. Kita harus punya aturan perpindahan yang jelas, atau akan terseret ke jalur yang makin rumit seperti
co_awaitdi C++ atau tokio. Menurut saya inilah ‘The Way’Ada trik sederhana untuk membuat semua fungsi menjadi merah, atau biru
Jika
iodijadikan variabel global, kita tak perlu khawatir soal coloring. Ini memang bercanda, tetapi memang ada sedikit friksi dari keharusan memakai antarmukaIo, hanya saja ini secara esensial berbeda dari friction nyata yang muncul saat memakai async/await. Menurut saya inti masalah function coloring adalah pewarnaan statis oleh keyword async yang membuat reuse kode jadi mustahil. Di Zig, baik fungsi dibuat async maupun tidak, semuanya menerima IO sebagai argumen, jadi dari sudut pandang itu coloring menjadi tak bermakna. Kedua, jika memakai async/await maka kita dipaksa memakai coroutine tanpa stack, yaitu perpindahan stack yang dikendalikan compiler, tetapi sistem IO baru Zig bisa berperilaku sebagai Blocking IO meskipun di dalamnya memakai async. Menurut saya inilah masalah function coloring yang sesungguhnyaGo juga mengalami masalah “coloring yang halus”. Saat memakai goroutine, kita harus terus menerus mengoper argumen context untuk menangani cancellation, dan banyak fungsi library juga meminta context, sehingga seluruh kode jadi terkontaminasi. Secara teknis kita memang bisa tidak memakai context, tetapi melempar
context.Backgroundsecara sembarangan bukanlah cara yang direkomendasikanKonsep sans-io sudah pernah dibahas di Rust dan lain-lain; referensinya adalah https://www.firezone.dev/blog/sans-io, https://sans-io.readthedocs.io/, dan https://news.ycombinator.com/item?id=40872020
Saya pikir masalah function coloring adalah, entah diproses di stack atau dengan unwind stack, pada akhirnya salah satunya tetap tersisa. Zig mengklaim menyelesaikan masalah coloring, tetapi dengan cara implementasi IO yang masih memungkinkan blocking/thread pool/green thread. Padahal blocking IO sejak awal bukan masalahnya. Selama kita menjaga kebiasaan tidak memakai global state, hampir semua bahasa bisa mencapai level ini. Stackless coroutine masih belum diimplementasikan, jadi rasanya seperti “tinggal menggambar sisa komponennya saja”. Jika benar-benar ingin pemanggilan fungsi yang universal, menurut saya ada dua cara
Membuat semua fungsi menjadi async, lalu dengan satu argumen menentukan apakah dijalankan secara sinkron atau tidak, lalu menanganinya di sana sendiri (ada penurunan performa)
Mengompilasi tiap fungsi dua kali lalu memilih pemanggilan yang sesuai konteksnya (ukuran kode membesar dan penanganan function pointer jadi sulit)
Saya bukan dari tim inti, tetapi saya dengar rencananya adalah membiarkan pengguna dan pemakai nyata cukup mencoba implementasi semiblocking, menstabilkan API, lalu menerapkan tepat solusi itu, yaitu memasukkan coroutine sungguhan berbasis lompatan stack. Saat ini compiler state machine coroutine LLVM bermasalah karena bergantung pada libc atau malloc. Antarmuka io baru Zig mendukung async/await di userland, jadi jika nanti solusi frame jumping yang tepat masuk, migrasinya akan mudah dan debugging juga nyaman. Jika coroutine ternyata sulit, API io juga dirancang agar tetap bertahan dengan sedikit revisi, jadi mereka tidak ingin terlalu tergesa-gesa mulai dari stackless coroutine
ValueTask<T>di C#/.NET juga berperan mirip. Jika selesai secara sinkron maka tidak ada overhead, dan hanya jika perlu akan dipakai sebagaiTask<T>. Kodenya biasanya cukup di-await, dan pada saat eksekusi runtime atau compiler akan memilih sinkron atau asinkron secara otomatisSaya suka Zig, tetapi agak disayangkan melihat fokusnya pada green thread, yaitu fiber atau stackful coroutine. Rust juga pernah membuang Runtime trait serupa sebelum 1.0 karena masalah performa. Faktanya, OS, bahasa, dan library sudah berkali-kali belajar tentang dampak buruk pendekatan ini, dan ada referensinya juga https://www.open-std.org/JTC1/SC22/WG21/docs/papers/2018/p1364r0.pdf. Fiber memang pernah dipuji pada 1990-an sebagai cara concurrency yang skalabel, tetapi di era modern tidak lagi direkomendasikan karena stackless coroutine serta perkembangan OS dan hardware. Jika terus begini, Zig akan mentok pada batas performa yang mirip Go dan akan sulit menjadi pesaing performa yang sesungguhnya. Saya berharap
std.fstetap ada untuk kasus yang membutuhkan performaKesan bahwa kami “all-in” pada green thread atau fiber adalah salah paham. Artikel rujukan OP dengan jelas menyebut bahwa implementasi berbasis stackless coroutine juga diharapkan, dan ada proposal terkait https://github.com/ziglang/zig/issues/23446. Performa itu penting, dan jika fiber ternyata tidak memenuhi harapan performa, itu tidak akan dipakai secara universal. Hal yang dibahas dalam artikel ini tidak menghalangi stackless coroutine menjadi implementasi
IodefaultSaya ragu dengan klaim bahwa green thread itu buruk untuk performa. Platform server concurrency tingkat atas seperti Go, Erlang, dan Java semuanya memakai atau berusaha memakai green thread. Green thread mungkin kurang cocok di bahasa yang lebih low-level seperti Rust karena masalah kompatibilitas dengan C FFI, tetapi sulit mengatakan performanya selalu menjadi masalah
Karena ini hanya salah satu dari beberapa opsi, saya tidak menganggapnya sebagai “all-in”. Implementasi mana yang dipilih ditentukan di executable, bukan di kode library
Zig tampaknya mengejar efek yang mirip dengan keputusan Rust membuang green thread lalu menggantinya dengan async runtime. Inti intuisi yang diformalkan adalah
async=IO, IO=async. Rust menyediakan pluggable async runtime seperti tokio, sedangkan Zig menyediakan pluggable IO runtime. Arah akhirnya adalah mengeluarkan runtime dari bahasa, membiarkannya dipasang di user space, sambil semua orang berbagi antarmuka yang samaDokumen itu (P1364R0) memang kontroversial, dan saya merasa argumennya dimotivasi untuk menyingkirkan pendekatan tertentu. Untuk bahan diskusi, bisa juga melihat https://old.reddit.com/r/cpp/comments/1jwlur9/stackful_coroutines_faster_than_stackless/, https://old.reddit.com/r/programming/comments/dgfxde/fibers_arent_useful_for_much_any_more/f3bmpww/ dan sebagainya
Saya merasa agak janggal bahwa bahasa sistem seperti Zig memaksakan polimorfisme runtime bahkan untuk operasi IO standar yang umum. Dalam banyak kasus nyata, implementasi IO bisa dipastikan secara statis, jadi saya bertanya-tanya mengapa harus memaksa overhead runtime
Untuk IO, saya rasa overhead dynamic dispatch pada praktiknya hampir selalu kecil. Tergantung target IO-nya, tentu, tetapi pada akhirnya jauh lebih sering IO bukanlah bottleneck CPU. Itulah sebabnya ada istilah IO-bound
Untuk pertanyaan “mengapa memaksa overhead runtime pada semua orang?”, tampaknya niatnya adalah pada sistem yang hanya memakai satu jenis io, compiler akan mengoptimalkan biaya
double indirectionitu sampai hilang. Dan karena IO biasanya punya bottleneck lain, tambahan satu kali indirection nyaris tidak membebaniDalam filosofi Zig, perhatian pada ukuran binary lebih besar. Allocator juga punya trade-off yang sama; misalnya
ArrayListUnmanagedtidak generic terhadap allocator, sehingga setiap alokasi memicu dynamic dispatch. Dalam praktiknya, biaya alokasi file atau penulisan jauh lebih besar daripada overhead pemanggilan tidak langsung. Obsesi pada ukuran binary seperti ini memang gaya Zig. Sebagai catatan,devirtualizationyaitu optimisasi yang mengubah pemanggilan dinamis menjadi statis, itu mitosPolimorfisme runtime itu sendiri tidak buruk secara esensial. Selama bukan situasi seperti munculnya branch di tight loop atau compiler tidak bisa melakukan optimisasi inline, biasanya itu bukan masalah
Saya memang tidak terlalu suka melihat parameter io baru muncul di mana-mana, tetapi saya sangat suka bahwa ini memudahkan pemakaian berbagai implementasi seperti berbasis thread atau fiber, dan tidak memaksa implementasi tertentu kepada pengguna, seperti antarmuka Allocator. Secara keseluruhan ini peningkatan besar, dan jika di antara berbagai implementasi stdlib disediakan implementasi io sinkron atau blocking tanpa overhead tambahan, itu benar-benar mengikuti filosofi Zig tentang “tidak membayar untuk yang tidak dipakai”
Di Zig,
io.asynchanya mengekspresikan asinkronitas, yaitu urutan kerja mungkin tidak terjamin tetapi hasilnya tetap benar, bukan concurrency. Jadi poin pentingnya adalah makna async dipisahkan dari makna pemanggilan io. Menurut saya desain ini sangat cerdasSaya suka bahwa antarmuka IO ini memungkinkan dibuatnya vfs (Virtual File System) pada level bahasa
Saya sempat mencoba membuat server ssh sederhana untuk belajar Zig. Berkat struktur IO/event loop kali ini, alur kodenya jadi jauh lebih mudah dipahami. Terima kasih untuk Andy
Tulisannya sangat bagus dan saya membacanya dengan sangat tertarik. Saya terutama antusias dengan implikasinya untuk WebAssembly. Gagasan bahwa WASI bisa dipakai di userspace, dan struktur ini juga mendukung Bring Your Own IO, benar-benar terasa menarik