2 poin oleh GN⁺ 2025-07-14 | 1 komentar | Bagikan ke WhatsApp
  • 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.async dan await
    • 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 await dan try digunakan bersama, ada masalah di mana jika terjadi error pada satu future, sumber daya dari future lain mungkin tidak sempat dilepas
  • Dengan defer dan future.cancel, pembatalan dan pembersihan dapat dinyatakan dengan jelas dan tepat

API Future.cancel

  • Future.cancel() dan Future.await() bersifat idempotent (dipanggil berkali-kali pun tidak menimbulkan efek samping)
  • Jika cancel dipanggil pada future yang sudah selesai, hanya sumber dayanya yang dibebaskan; untuk pekerjaan yang belum selesai, akan dikembalikan error.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_uring di 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 parameter splat, 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

 
GN⁺ 2025-07-14
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 Io di Zig bukan sekadar nilai khusus untuk pemrosesan asinkron, melainkan nilai yang memang diperlukan untuk semua IO seperti membaca file, sleep, mengambil waktu, dan sebagainya. Io bukan 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 dari my_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 kira Io juga tidak akan jadi masalah besar

    • Menurut 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_await di C++ atau tokio. Menurut saya inilah ‘The Way’

    • Ada trik sederhana untuk membuat semua fungsi menjadi merah, atau biru

      var io: std.Io = undefined;
      
      pub fn main() !void {
        var impl = ...;
        io = impl.io();
      }
      

      Jika io dijadikan variabel global, kita tak perlu khawatir soal coloring. Ini memang bercanda, tetapi memang ada sedikit friksi dari keharusan memakai antarmuka Io, 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 sesungguhnya

    • Go 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.Background secara sembarangan bukanlah cara yang direkomendasikan

  • Konsep 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

    • Jika sebuah fungsi langsung memanggil metode IO, strukturnya sulit dipisahkan dari IO dari luar, jadi saya rasa sulit menyebutnya sans-io. Seperti dijelaskan di tautan, pada protokol berbasis byte stream, implementasinya seharusnya hanya menangani buffer input/output, dan bagian yang menerima data dari jaringan harus benar-benar diteruskan langsung oleh pihak pemanggil agar bisa disebut sans-io yang sesungguhnya. Untuk output pun bisa hanya menulis ke buffer, atau langsung mengembalikan byte stream ketika event terjadi. Cara pengembaliannya adalah pilihan implementasi, tetapi buffer internal berguna untuk situasi yang membutuhkan respons otomatis. Intinya adalah struktur yang tidak melakukan IO secara langsung
  • 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 sebagai Task<T>. Kodenya biasanya cukup di-await, dan pada saat eksekusi runtime atau compiler akan memilih sinkron atau asinkron secara otomatis

  • Saya 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.fs tetap ada untuk kasus yang membutuhkan performa

    • Kesan 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 Io default

    • Saya 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 sama

    • Dokumen 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 indirection itu sampai hilang. Dan karena IO biasanya punya bottleneck lain, tambahan satu kali indirection nyaris tidak membebani

    • Dalam filosofi Zig, perhatian pada ukuran binary lebih besar. Allocator juga punya trade-off yang sama; misalnya ArrayListUnmanaged tidak 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, devirtualization yaitu optimisasi yang mengubah pemanggilan dinamis menjadi statis, itu mitos

    • Polimorfisme 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”

    • Apakah “tidak membayar untuk yang tidak dipakai” benar-benar mungkin? Kecuali timnya sangat kecil dan super disiplin, pada akhirnya orang lain akan memakainya dan saya juga tetap menanggung biayanya. Dan terus menerus meneruskan io rasanya lebih merepotkan daripada sekadar memanggil langsung ketika memang dibutuhkan
  • Di Zig, io.async hanya 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 cerdas

  • Saya suka bahwa antarmuka IO ini memungkinkan dibuatnya vfs (Virtual File System) pada level bahasa

    • Melihat contoh kodenya, dari sudut pandang keamanan saya jadi berpikir apakah ini juga bisa dipakai untuk keamanan berbasis capability. Misalnya dengan memberikan ke library sebuah instance io yang hanya bisa membaca di bawah direktori tertentu. Lihat juga https://news.ycombinator.com/item?id=44549430
  • 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

    • Saya penasaran bagian apa dari desain baru ini yang membuat event loop atau io jadi lebih mudah dipahami
  • 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