Semua klien dan server yang kita tulis sepanjang section ini memakai model blocking: recv() menahan thread sampai data tiba, dan curl_easy_perform() menahan thread sampai transfer selesai — jujur dan mudah diikuti untuk satu koneksi. Untuk server yang melayani ribuan koneksi bersamaan, membuka satu thread per koneksi berarti membuka ribuan stack thread yang sebagian besar waktunya habis untuk menunggu. Chapter ini memperkenalkan Asio, pustaka C++ standar de facto untuk I/O asinkron: daftarkan operasi beserta handler-nya, lalu biarkan event loop memanggil handler saat data siap. Contoh Asio ditampilkan sebagai sketsa berlabel ilustratif; program yang diuji penuh di chapter ini hanya satu, dan tidak butuh pustaka apa pun.

Masalah yang Dijawab IO Asinkron #

Cara paling intuitif menangani banyak koneksi adalah satu thread per koneksi: setiap thread memanggil recv() blocking dan tidak perlu memikirkan apa pun lagi. Model ini bekerja sampai puluhan koneksi; setelah itu biayanya muncul di dua tempat — setiap thread membawa stack sendiri di urutan megabyte, dan penjadwal OS sibuk berganti konteks untuk thread yang sebenarnya hanya menunggu. Skala masalah ini cukup populer sampai punya nama: C10K, sepuluh ribu koneksi bersamaan.

IO asinkron membalik caranya: program meminta operasi tanpa menunggu — “baca socket ini, dan panggil handler saya saat datanya siap”. Satu thread cukup menjalankan event loop yang menerima pemberitahuan OS (di Linux melalui epoll, di Windows melalui I/O completion ports) lalu memanggil handler yang sudah siap; ribuan koneksi kini hanya berupa data dalam tabel, bukan ribuan stack thread. Asio menyatukan mekanisme OS itu ke satu API lintas platform, dan situs resminya merangkum tujuannya:

“Asio is a cross-platform C++ library for network and low-level I/O programming that provides developers with a consistent asynchronous model using a modern C++ approach.” — think-async.com

Model Asio: io_context, Handler, dan Timer #

Tiga suku kata menopang seluruh Asio. io_context adalah event loop-nya: objek yang menyimpan daftar operasi yang menunggu, dengan fungsi run() yang mengeksekusi handler yang sudah siap sampai tidak ada pekerjaan tersisa. Operasi asinkron adalah permintaan berawalan async_ — async_wait untuk timer, async_read_some untuk socket — dan handler-nya fungsi biasa yang dipanggil Asio saat keadaan yang ditunggu terjadi. Contoh pertama, timer:

cpp Sketsa Asio — timer asinkron (ILUSTRATIF: pustaka tidak terinstal di sandbox, bentuk API untuk proyek nyata)
#include <asio.hpp>   // standalone Asio; Boost.Asio memakai <boost/asio.hpp>
#include <chrono>
#include <iostream>

int main() {
    asio::io_context io;   // event loop: objek yang menjalankan handler

    asio::steady_timer timer(io, std::chrono::seconds(1));
    timer.async_wait([](const asio::error_code& e) {  // handler callback
        if (!e) std::cout << "1 detik berlalu\n";
    });

    io.run();  // eksekusi handler yang siap; kembali saat tidak ada pekerjaan
    return 0;
}

Program itu membentuk siklus yang khas: operasi didaftarkan lewat async_wait(), io.run() menjalankan event loop, dan satu detik kemudian Asio memanggil lambda handler kita — tanpa thread tambahan dan tanpa tidur manual. Membaca socket mengikuti pola yang sama, dengan satu perbedaan: data datang terus-menerus, sehingga handler memanggil ulang operasi asinkron dan membentuk rantai:

cpp Sketsa Asio — membaca socket secara asinkron (ILUSTRATIF: bentuk API untuk proyek nyata)
void baca(asio::ip::tcp::socket& socket, std::vector<char>& buffer) {
    socket.async_read_some(asio::buffer(buffer),
        [&socket, &buffer](const asio::error_code& e, std::size_t n) {
            if (e) return;         // galat atau koneksi ditutup (EOF)
            proses(buffer, n);     // n byte sudah tersedia
            baca(socket, buffer);  // minta potongan berikutnya: callback berantai
        });
}

Bandingkan dengan loop recv() blocking di chapter TCP Socket dengan POSIX Socket API: pekerjaannya sama, tetapi bentuknya berubah dari loop menjadi rantai callback — setiap tahap menerima n byte yang sudah pasti tersedia, lalu mendaftarkan tahap berikutnya.

Program Utama: Event Loop Mini #

Inti io_context bisa ditiru tanpa socket dan tanpa pustaka: priority queue berisi tugas berjatuh tempo, plus fungsi yang mengeksekusinya berurutan. Program berikut membangun event loop itu dengan satu keputusan penting untuk pengujian — waktu disimulasikan: tidak ada thread nyata dan tidak ada tidur, variabel waktu melompat langsung ke jatuh tempo tugas berikutnya, sehingga output deterministik penuh. Pemetaannya ke Asio kentara: antrian tugas adalah daftar handler, jatuh tempo memainkan peran async_wait(), dan aksi yang menjadwalkan tugas baru meniru handler yang memanggil operasi asinkron berikutnya.

cpp
#include <cstdint>       // uint64_t
#include <functional>    // std::function
#include <iostream>
#include <queue>         // std::priority_queue
#include <string>
#include <utility>       // std::move
#include <vector>        // penampung dasar bawaan priority_queue

// Event loop mini yang meniru pola io_context: tugas dijadwalkan dengan
// delay logis, lalu dieksekusi berurutan dari jatuh tempo paling awal.
// Waktu di sini SIMULASI (milidetik logis), tanpa thread dan tanpa tidur,
// sehingga urutan output selalu sama di setiap eksekusi.
class EventLoop {
public:
    // Jadwalkan aksi pada waktu sekarang + delayMs. Nama dipakai untuk
    // jejak output; aksi boleh menjadwalkan tugas baru selama loop jalan.
    void jadwal(long long delayMs, std::string nama, std::function<void()> aksi) {
        tugas_.push(Tugas{waktuSekarang_ + delayMs, nomorUrut_++,
                          std::move(nama), std::move(aksi)});
    }

    // Analogi io_context::run(): habiskan antrian, eksekusi berurutan.
    void jalankan() {
        while (!tugas_.empty()) {
            Tugas t = tugas_.top();
            tugas_.pop();
            waktuSekarang_ = t.jatuhTempo;  // waktu logis "melompat" ke tugas
            std::cout << "[t=" << waktuSekarang_ << " ms] " << t.nama << "\n";
            t.aksi();  // aksi dieksekusi setelah jejaknya dicetak
        }
    }

private:
    struct Tugas {
        long long jatuhTempo;       // waktu logis eksekusi
        uint64_t nomorUrut;         // pemecah seri: tugas seri jadi FIFO
        std::string nama;
        std::function<void()> aksi;

        // priority_queue adalah max-heap: operator< membalik perbandingan
        // supaya tugas dengan jatuh tempo PALING AWAL berada di puncak.
        bool operator<(const Tugas& lain) const {
            if (jatuhTempo != lain.jatuhTempo) {
                return jatuhTempo > lain.jatuhTempo;
            }
            return nomorUrut > lain.nomorUrut;
        }
    };

    std::priority_queue<Tugas> tugas_;
    long long waktuSekarang_ = 0;
    uint64_t nomorUrut_ = 0;
};

int main() {
    EventLoop loop;

    // Empat tugas dijadwalkan tidak berurutan; dua di antaranya seri di 50 ms
    loop.jadwal(150, "timer A: sinkronisasi data", [] {});
    loop.jadwal(50, "timer B: kirim heartbeat", [] {});
    loop.jadwal(50, "timer C: periksa antrean", [] {});

    // Tugas berantai: saat D dieksekusi, D menjadwalkan E dengan delay baru
    loop.jadwal(100, "timer D: mulai, lalu jadwalkan tindak lanjut", [&loop] {
        loop.jadwal(120, "timer E: tindak lanjut dari D", [] {});
    });

    loop.jalankan();
    std::cout << "antrian kosong: jalankan() kembali seperti io_context::run()\n";
    return 0;
}

Output yang Dihasilkan:

text
[t=50 ms] timer B: kirim heartbeat
[t=50 ms] timer C: periksa antrean
[t=100 ms] timer D: mulai, lalu jadwalkan tindak lanjut
[t=150 ms] timer A: sinkronisasi data
[t=220 ms] timer E: tindak lanjut dari D
antrian kosong: jalankan() kembali seperti io_context::run()

Mari kita bedah baris demi baris:

  1. Baris 17-20 (jadwal): tugas dicatat dengan jatuh tempo waktuSekarang_ + delayMs — delay dihitung dari waktu logis saat itu, bukan dari jam dinding. nomorUrut_ yang terus bertambah menjadi pemecah seri.
  2. Baris 23-31 (jalankan): analogi io_context::run(). Loop mengambil tugas terdepan, waktu logis “melompat” langsung ke jatuh tempo tugas itu, jejaknya dicetak, lalu aksi dieksekusi. Aksi boleh memanggil jadwal() lagi — antrian bertambah selama loop berjalan.
  3. Baris 42-47 (operator<): std::priority_queue adalah max-heap, sehingga perbandingannya dibalik: tugas dengan jatuh tempo paling awal ditempatkan di puncak. Saat dua tugas jatuh tempo bersamaan, nomorUrut lebih kecil menang — eksekusi menjadi FIFO sesuai urutan penjadwalan.
  4. Baris 50-52 (state event loop): tiga anggota data cukup — antrian tugas, waktu logis, dan penghitung nomor urut. Seluruh perilaku io_context versi mini berdiri di atasnya.
  5. Baris 59-61 (penjadwalan tak berurut): timer A dijadwalkan lebih dulu dengan delay 150 ms, lalu B dan C dengan 50 ms. Urutan pemanggilan jadwal() tidak menentukan urutan eksekusi — waktu jatuh tempo yang menentukan.
  6. Baris 64-66 (tugas berantai): saat D dieksekusi pada t=100, aksinya menjadwalkan E dengan delay 120 ms, sehingga jatuh tempo E menjadi 100 + 120 = 220 ms. Inilah pola handler yang mendaftarkan operasi asinkron berikutnya — persis rantai async_read_some pada sketsa sebelumnya.
  7. Baris 68-69 (akhir loop): jalankan() kembali saat antrian kosong, sama seperti io_context::run() yang kembali ketika tidak ada handler tersisa untuk dieksekusi.

Urutan output menegaskan aturan mainnya: B dan C berbagi t=50 dan dieksekusi FIFO, A yang dijadwalkan pertama justru menunggu sampai t=150, dan E — yang baru lahir saat D berjalan — mengisi t=220. Dengan thread nyata dan tidur sungguhan urutannya tetap sama tetapi pengujian menjadi lambat; dengan waktu simulasi, seluruh skenario 220 milidetik selesai seketika.

Callback vs Coroutine C++20 #

Rantai callback punya kelemahan yang terasa begitu logikanya bercabang: setiap tahap menjadi fungsi baru dan alur program terpecah di antara handler. C++20 menyediakan jalan kedua — coroutine, yang cppreference definisikan:

“A coroutine is a function that can suspend execution to be resumed later. […] This allows for sequential code that executes asynchronously (e.g. to handle non-blocking I/O without explicit callbacks).” — en.cppreference.com, Coroutines

Chapter Coroutines: Pengantar mengenalkan co_yield dan co_return lewat std::generator, sambil menyisihkan co_await karena kata kunci itu butuh pustaka tambahan — dan pustaka tambahan itu adalah Asio: tipe asio::awaitable menjadikan setiap operasi async_* bisa di-co_await. Versi coroutine dari pembaca socket di atas berhenti terlihat seperti rantai:

cpp Sketsa Asio — echo server dengan coroutine C++20 (ILUSTRATIF: bentuk API untuk proyek nyata)
asio::awaitable<void> echo(asio::ip::tcp::socket socket) {
    char data[1024];
    for (;;) {
        std::size_t n = co_await socket.async_read_some(asio::buffer(data));
        co_await asio::async_write(socket, asio::buffer(data, n));
    }
}

Bentuknya menyerupai loop recv() di chapter TCP — terbaca dari atas ke bawah seperti kode sekuensial biasa; perbedaannya di makna co_await: fungsi menangguhkan dirinya saat data belum siap, thread yang sama bebas menjalankan handler lain, dan eksekusi dilanjutkan dari titik penangguhan ketika data tiba. Fitur bahasanya sudah tersedia di GCC 14 praktikum ini, tetapi pustaka Asio-nya tidak terinstal — sketsa di atas tetap menunggu proyek nyata Anda.

Dua Wajah Asio: Standalone dan Boost.Asio #

Situs resminya menyatakan: “Asio comes in two variants: (non-Boost) Asio and Boost.Asio.” Keduanya berbagi API inti yang sama — kode dengan io_context, timer, dan socket cukup berpindah namespace dan header — tetapi siklus rilis dan paketnya berbeda. Boost.Asio menarik bila proyek sudah memakai Boost; standalone Asio menarik bila dependensi satu Boost terasa besar untuk satu pustaka jaringan.

AspekStandalone AsioBoost.Asio
Namespaceasio::boost::asio::
Header<asio.hpp><boost/asio.hpp>
Rilismengikuti siklus rilis Asio sendiriterikat siklus rilis Boost
Cocok saatproyek tanpa dependensi Boostproyek yang sudah memakai Boost

Mengambil Asio bukan pekerjaan berat — pustakanya header-only dan tersedia di kanal paket populer:

KanalCara mengambil
vcpkgport asio — vcpkg install asio
ConanCenterconan install --requires="asio/1.38.2"
FetchContentrepositori GitHub resmi, tag asio-1-38-2
Boostport boost-asio di vcpkg, atau Boost yang terinstal sistem
cmake Sketsa FetchContent untuk Asio (ilustratif; tag asio-1-38-2 ada di repositori resmi)
include(FetchContent)
FetchContent_Declare(asio
    GIT_REPOSITORY https://github.com/chriskohlhoff/asio.git
    GIT_TAG asio-1-38-2
)
FetchContent_MakeAvailable(asio)

target_link_libraries(serverku PRIVATE asio::asio)

Setelah target tersedia, latihan yang jujur adalah menulis ulang dua program teruji kita — klien framing dan klien HTTP mentah — di atas Asio: perilakunya sudah kita pahami, sehingga perbedaan model blocking dan asinkron langsung terlihat.

Section Network Programming berakhir di sini dengan bekal runtut: model client-server, socket TCP dan UDP, framing, HTTP, dan model asinkron untuk skala besar. Section berikutnya berpindah ke perhitungan numerik, tetapi pola yang baru kita uji — antrian handler, waktu yang bisa disimulasikan — akan muncul kembali di banyak pustaka C++ modern.

Referensi: think-async.com — Asio C++ Library , cppreference — Coroutines