Kisah async secara jujur

Memahami sejarah async/await yang dihapus dan model Io yang menggantikannya.

Daftar isi

Model I/O 0.17 tidak lahir dari keputusan tunggal, melainkan dari satu kegagalan besar yang diakui secara terbuka. Fitur async/await yang semula direncanakan menjadi tulang punggung konkurensi Zig mengalami kemunduran dan akhirnya dihapus, kemudian digantikan oleh antarmuka std.Io yang diteruskan secara eksplisit. Topik ini menceritakan kedua fase tersebut secara berurutan, menunjukkan bentuk penggantinya dalam kode, dan menegaskan posisi pembelajar yang mengikuti materi lama.

Konsep#

Eksperimen async/await yang dihapus#

Zig pernah memuat kata kunci async dan await untuk menulis fungsi yang dapat ditangguhkan dan dilanjutkan oleh pustaka, sehingga satu fungsi dapat berjalan tak sinkron tanpa thread terpisah. Eksperimen ini mencapai puncaknya pada era 0.11.0, tetapi catatan rilis 0.12.0 menyatakan bahwa fungsi async mengalami kemunduran sejak 0.11.0 dan masa depannya dalam bahasa Zig tidak pasti. Empat penyebabnya dicantumkan secara eksplisit, yaitu ketidakmampuan LLVM mengoptimalkannya, ketidakmampuan debugger pihak ketiga menelusurinya, persoalan pembatalan (cancellation) yang belum terpecahkan, dan fungsi async sebagai pointer yang membuat ukuran stack tidak dapat diketahui.

Keputusan yang diambil adalah menghapus fiturnya alih-alih mempertahankan eksperimen yang berat bebannya, dan perombakannya memakan waktu beberapa rilis. Pada 0.17.0, async sudah bukan kata kunci lagi, karena kode dengan bentuk fn lama() async void ditolak kompilator sebagai error sintaks. Nama async tetap hidup, tetapi kini sebagai nama fungsi biasa pada antarmuka Io, sehingga sejarahnya masih terbaca langsung pada pustaka standar.

Model pengganti: instance std.Io#

Penggantinya bukan kata kunci baru, melainkan sebuah instance. std.Io adalah antarmuka yang mencakup sistem berkas, jaringan, proses, waktu, hingga penjadwalan tugas, dan setiap operasinya menerima instance tersebut sebagai parameter. Fungsi main berparameter std.process.Init menyediakannya melalui init.io, sedangkan kode start toolchain mengisi nilai itu dengan std.Io.Threaded, yaitu implementasi berbasis kolam thread kerja. Karena instance diteruskan secara eksplisit, fungsi yang memerlukan I/O menyatakannya pada tanda tangannya, selaras dengan cara Zig menuntut parameter allocator.

Program berikut memperlihatkan pola penerusan tersebut, yaitu instance io diambil satu kali di main lalu dikirim ke fungsi bantu yang membutuhkannya.

zig teruskan-io.zig
const std = @import("std");

fn catat(io: std.Io, pesan: []const u8) !void {
    var out_buf: [256]u8 = undefined;
    var stdout_writer = std.Io.File.stdout().writer(io, &out_buf);
    const out = &stdout_writer.interface;
    try out.print("catatan: {s}\n", .{pesan});
    try out.flush();
}

pub fn main(init: std.process.Init) !void {
    const io = init.io;
    try catat(io, "sensor D3 aktif");
    try catat(io, "arsip ditutup");
}
text Keluaran
catatan: sensor D3 aktif
catatan: arsip ditutup

Fungsi catat menerima io: std.Io sebagai parameter pertama dan memakainya untuk membangun Writer atas stdout. Tidak ada variabel global yang menyimpan kemampuan I/O secara tersembunyi, sehingga pembaca kode dapat mengikuti setiap pemakaian I/O dari tanda tangan fungsinya. Pemanggil pula yang menentukan implementasinya, dan fungsi bantu tetap bekerja dengan implementasi Io mana pun yang diberikan.

Jalankan program dari direktori tempat teruskan-io.zig berada.

bash
# jalankan program dari direktori topik
zig run teruskan-io.zig
text Keluaran
catatan: sensor D3 aktif
catatan: arsip ditutup

Posisi pembelajar#

Konsekuensi langsung bagi pembelajar adalah seluruh kode I/O yang ditulis untuk 0.15.0 ke bawah tidak berlaku. Pada 0.17.0, namespace std.io tidak ada lagi dan std.fs.File juga hilang, sehingga contoh lama yang memanggil getStdOut atau fixedBufferStream gagal dikompilasi, bukan sekadar memunculkan peringatan. Materi pada jalur ini seluruhnya ditulis dan diverifikasi untuk 0.17.0, sedangkan materi lama perlu diperlakukan sebagai sejarah, bukan acuan.

Catatan rilis menjadi sumber kebenaran untuk menelusuri transisi ini. Catatan 0.15.0 memperkenalkan antarmuka Reader dan Writer baru beserta perombakan sistem berkasnya, dan catatan 0.17.0 melanjutkannya hingga namespace std.io lama tidak tersisa. Jalur pembaruan untuk kode berkas sederhana pun ringkas, yaitu file.close() menjadi file.close(io), sehingga perbedaan versinya besar pada tampilan kode, tetapi pola dasarnya dapat dipelajari dengan cepat.

Contoh Lengkap#

Konkurensi melalui antarmuka Io#

Konkurensi ikut pindah ke antarmuka. Panggilan io.async(function, args) menjadwalkan pemanggilan sebuah fungsi dan mengembalikan Future, metode future.await(io) menunggu hasilnya, sedangkan future.cancel(io) meminta pembatalan tugasnya. Operasi yang dapat dibatalkan memuat error.Canceled dalam error set-nya, dan tipe Io.Cancelable menandai himpunan tersebut. Kata async di sini adalah fungsi biasa milik instance, bukan kata kunci bahasa, sehingga perilakunya bergantung pada implementasi Io yang dipakai.

Program berikut menjadwalkan dua tugas kecil yang menghitung panjang teks, menunggu keduanya, lalu mencetak hasilnya.

zig tugas-berjangka.zig
const std = @import("std");

fn ukuranTeks(teks: []const u8) usize {
    return teks.len;
}

pub fn main(init: std.process.Init) !void {
    const io = init.io;

    var out_buf: [512]u8 = undefined;
    var stdout_writer = std.Io.File.stdout().writer(io, &out_buf);
    const out = &stdout_writer.interface;

    var tugas_pagi = io.async(ukuranTeks, .{"suhu=27.5"});
    var tugas_sore = io.async(ukuranTeks, .{"lembap=61%"});
    defer {
        _ = tugas_pagi.cancel(io);
        _ = tugas_sore.cancel(io);
    }

    const n_pagi = tugas_pagi.await(io);
    const n_sore = tugas_sore.await(io);

    try out.print("catatan pagi: {d} byte\n", .{n_pagi});
    try out.print("catatan sore: {d} byte\n", .{n_sore});
    try out.flush();
}
text Keluaran
catatan pagi: 9 byte
catatan sore: 10 byte

Dua tugas dijadwalkan lewat io.async, hasilnya diambil dengan await, dan blok defer memanggil cancel sebagai jaminan pembersihan karena kedua metode tersebut aman dipanggil pada Future yang sudah selesai. Pada implementasi Io yang sesungguhnya, kedua tugas dapat berjalan pada thread yang berbeda. Pada implementasi uji, tugas yang sama dapat dieksekusi secara langsung tanpa thread, sehingga kode konkuren tetap dapat diuji dengan deterministik.

Jalankan program dari direktori tempat tugas-berjangka.zig berada.

bash
# jalankan program dari direktori topik
zig run tugas-berjangka.zig
text Keluaran
catatan pagi: 9 byte
catatan sore: 10 byte

Kesalahan umum#

Kesalahan pertama berupa mencari kata kunci async, await, atau suspend pada 0.17.0 dan menyimpulkan Zig telah kehilangan fitur. Kata kunci itu memang dihapus bersama eksperimen fungsinya, tetapi konkurensi tetap tersedia melalui instance Io dan antarmuka fungsinya. Program yang berhenti pada pesan kata kunci tidak dikenal biasanya menyalin kode dari sumber pra-0.15, dan penyesuaiannya berpindah ke model instance.

Kesalahan kedua berupa menyamakan instance Io dengan thread. Instance Io adalah cara program berbicara dengan operasi menunggu, sedangkan thread adalah alur eksekusi sistem operasi, dan keduanya dipakai bersama tanpa saling menggantikan. Program yang membuat thread tanpa instance Io tetap sah, hanya saja operasi yang menunggu tidak memperoleh jalan pintas yang disediakan instance.

Ringkasan#

  • Eksperimen async/await era 0.11.0 mengalami kemunduran karena masalah optimasi LLVM, dukungan debugger, pembatalan, dan ukuran stack, lalu dihapus dari bahasa.
  • Penggantinya adalah instance std.Io yang diperoleh dari init.io dan diteruskan eksplisit ke setiap operasi, dengan std.Io.Threaded sebagai implementasi bawaan.
  • Konkurensi berpindah ke antarmuka, yaitu io.async menghasilkan Future, await(io) menunggu hasilnya, dan cancel(io) meminta pembatalan melalui error.Canceled.
  • Kode I/O 0.15.0 ke bawah tidak berlaku pada 0.17.0 karena std.io dan std.fs.File sudah tidak ada.

Referensi#