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:
#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:
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.
#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:
[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:
- Baris 17-20 (
jadwal): tugas dicatat dengan jatuh tempowaktuSekarang_ + delayMs— delay dihitung dari waktu logis saat itu, bukan dari jam dinding.nomorUrut_yang terus bertambah menjadi pemecah seri. - Baris 23-31 (
jalankan): analogiio_context::run(). Loop mengambil tugas terdepan, waktu logis “melompat” langsung ke jatuh tempo tugas itu, jejaknya dicetak, lalu aksi dieksekusi. Aksi boleh memanggiljadwal()lagi — antrian bertambah selama loop berjalan. - Baris 42-47 (
operator<):std::priority_queueadalah max-heap, sehingga perbandingannya dibalik: tugas dengan jatuh tempo paling awal ditempatkan di puncak. Saat dua tugas jatuh tempo bersamaan,nomorUrutlebih kecil menang — eksekusi menjadi FIFO sesuai urutan penjadwalan. - Baris 50-52 (state event loop): tiga anggota data cukup — antrian tugas, waktu logis, dan penghitung nomor urut. Seluruh perilaku
io_contextversi mini berdiri di atasnya. - Baris 59-61 (penjadwalan tak berurut):
timer Adijadwalkan lebih dulu dengan delay 150 ms, lalu B dan C dengan 50 ms. Urutan pemanggilanjadwal()tidak menentukan urutan eksekusi — waktu jatuh tempo yang menentukan. - 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_somepada sketsa sebelumnya. - Baris 68-69 (akhir loop):
jalankan()kembali saat antrian kosong, sama sepertiio_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:
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.
| Aspek | Standalone Asio | Boost.Asio |
|---|---|---|
| Namespace | asio:: | boost::asio:: |
| Header | <asio.hpp> | <boost/asio.hpp> |
| Rilis | mengikuti siklus rilis Asio sendiri | terikat siklus rilis Boost |
| Cocok saat | proyek tanpa dependensi Boost | proyek yang sudah memakai Boost |
Mengambil Asio bukan pekerjaan berat — pustakanya header-only dan tersedia di kanal paket populer:
| Kanal | Cara mengambil |
|---|---|
| vcpkg | port asio — vcpkg install asio |
| ConanCenter | conan install --requires="asio/1.38.2" |
| FetchContent | repositori GitHub resmi, tag asio-1-38-2 |
| Boost | port boost-asio di vcpkg, atau Boost yang terinstal sistem |
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