Materi ini melanjutkan pembahasan I/O model (blocking/non-blocking, sync/async) ke level praktik: bagaimana asynchronous/event-based programming benar-benar diimplementasikan lewat library event loop — baik di level C (libevent, libev, libuv) maupun di Python (asyncio).
Motivasi: Mengapa Asynchronous Programming Dibutuhkan
Untuk aplikasi I/O bound, khususnya penanganan network, pendekatan asynchronous/event-based programming dapat lebih efisien dibanding pendekatan thread-per-request. Efisiensi ini datang dari penggunaan resource:
- Tidak perlu alokasi stack tersendiri untuk setiap thread.
- Tidak perlu terjadi context switching saat penanganan resource (karena tidak ada banyak thread yang saling bergantian dijadwalkan OS).
- Menghindari problem race condition yang muncul pada multitasking berbasis thread (karena logika berjalan pada satu alur eksekusi yang mengeksekusi handler satu per satu).
Dua catatan penting yang perlu diluruskan supaya tidak salah paham soal asynchronous programming:
Asynchronous tidak membuat program menjadi lebih cepat. Asynchronous tidak berarti tidak perlu menggunakan thread lagi.
Artinya, async adalah soal efisiensi penggunaan resource (terutama saat menangani banyak koneksi I/O bersamaan), bukan soal mempercepat komputasi — dan thread tetap bisa/perlu dipakai untuk pekerjaan yang memang blocking atau CPU-bound (lihat bagian Executor di bawah).
Arsitektur Library Asynchronous: Event Loop dan Handler
Ide dasar dari semua library asynchronous adalah mengubah blocking request menjadi event loop dan handler:
- Event loop bertugas mengecek apakah ada event yang terjadi, lalu memanggil handler yang sesuai dengan event tersebut.
- Handler tidak boleh melakukan operasi blocking, dan harus bisa selesai secepat mungkin — karena selama handler berjalan, event loop tidak bisa memproses event lain. Jika ada blocking request yang harus dilakukan (misal I/O lambat atau komputasi berat), request tersebut harus “dialihkan” keluar dari alur event loop (misalnya lewat thread pool), bukan dieksekusi langsung di dalam handler.
Ketika mempelajari sebuah framework/library asynchronous, ada beberapa hal yang perlu dipahami:
- Bagaimana event loop dibuat, dijalankan/diaktifkan, dan terminasinya.
- Bagaimana cara mendefinisikan event dan handler-nya.
- Bagaimana mendaftarkan event dan handler-nya ke event loop.
- Apa saja jenis handler yang disediakan.
- Bagaimana menangani blocking request.
- Apakah ada library pendukung yang disediakan untuk high level task.
Ketiga poin pertama sampai kelima ini akan dibahas konkret lewat libev (C) di bagian berikut, dan lewat asyncio (Python) di bagian sesudahnya — keduanya menerapkan pola arsitektur yang persis sama, hanya beda bahasa dan API.
Library Event Loop di C: libevent, libev, libuv
Ada tiga library event loop populer yang saling berkaitan secara historis:
- libevent (libevent.org) — library yang menyediakan mekanisme untuk penanganan event, dan memanggil callback yang sesuai. Mendukung banyak mekanisme I/O multiplexing di level OS:
/dev/poll,kqueue,select,poll,epoll. - libev (software.schmorp.de/pkg/libev.html) — dikembangkan untuk memperbaiki bug dan limitasi yang ada pada libevent. Digambarkan sebagai “a full-featured and high-performance event loop that is loosely modelled after libevent, but without its limitations and bugs”.
- libuv (github.com/libuv/libuv) — dikembangkan untuk digunakan pada Node.js. Merupakan abstraksi di atas libev pada Linux dan IOCP (I/O Completion Port) pada Windows, sehingga aplikasi yang dibangun di atasnya portable lintas platform.
Fungsi Dasar Event Loop
Secara konseptual, sebuah event loop bekerja dengan: mendefinisikan interest terhadap sebuah event, lalu menyediakan handler/watcher untuk penanganan event tersebut ketika event itu terjadi.
Anatomi Watcher pada libev
Watcher adalah struktur yang dialokasikan untuk menyatakan minat (interest) terhadap sebuah event tertentu. Misalnya, untuk menangani event pada keyboard (stdin), dibuat watcher jenis ev_io:
static void my_cb (struct ev_loop *loop, ev_io *w, int revents)
{
ev_io_stop (w);
ev_break (loop, EVBREAK_ALL);
}
struct ev_loop *loop = ev_default_loop (0);
ev_io stdin_watcher;
ev_init (&stdin_watcher, my_cb);
ev_io_set (&stdin_watcher, STDIN_FILENO, EV_READ);
ev_io_start (loop, &stdin_watcher);
ev_run (loop, 0);Alur di atas menunjukkan pola umum penggunaan watcher di libev: buat event loop (ev_default_loop), inisialisasi watcher dengan callback-nya (ev_init), konfigurasikan watcher itu — file descriptor mana dan event apa yang diminati (ev_io_set), aktifkan watcher pada loop (ev_io_start), lalu jalankan loop-nya (ev_run). Di dalam callback, watcher yang sifatnya one-shot harus dihentikan manual (ev_io_stop), dan ev_break dipakai untuk menghentikan loop.
Setiap jenis watcher (ev_TYPE) di libev punya keluarga fungsi dengan pola nama yang konsisten:
ev_init— inisialisasi umum watcher dan callback-nya.ev_TYPE_set— mengatur parameter spesifik jenis watcher tersebut.ev_TYPE_init— gabunganev_init+ev_TYPE_setdalam satu pemanggilan.ev_TYPE_start— mendaftarkan watcher ke event loop supaya aktif dipantau.ev_TYPE_stop— menghentikan/melepas watcher dari event loop.
libev menyediakan banyak jenis watcher, masing-masing untuk jenis event yang berbeda:
- io (
ev_io) — I/O readiness pada file descriptor, bersifat level trigger. - timer (
ev_timer) — event setelah durasi waktu tertentu. - periodic (
ev_periodic) — event berulang pada interval waktu tertentu. - signal (
ev_signal) — menangani signal OS. - child (
ev_child) — memantau status child process. - stat (
ev_stat) — memantau perubahan pada file/filesystem. - idle (
ev_idle) — dipanggil ketika event loop sedang idle (tidak ada event lain). - prepare (
ev_prepare) dan check (ev_check) — dipanggil tepat sebelum dan sesudah loop melakukan polling I/O. - embed (
ev_embed) — menyisipkan (embed) satu event loop ke event loop lain. - fork (
ev_fork) — dipanggil setelah proses melakukanfork().
Contoh Program Lengkap libev
Contoh berikut menggabungkan dua jenis watcher sekaligus — ev_io untuk stdin dan ev_timer untuk timeout — dalam satu program:
// a single header file is required
#include <ev.h>
#include <stdio.h> // for puts
// every watcher type has its own typedef'd struct
// with the name ev_TYPE
ev_io stdin_watcher;
ev_timer timeout_watcher;
// all watcher callbacks have a similar signature
// this callback is called when data is readable on stdin
static void
stdin_cb (EV_P_ ev_io *w, int revents)
{
puts ("stdin ready");
// for one-shot events, one must manually stop the watcher
// with its corresponding stop function.
ev_io_stop (EV_A_ w);
// this causes all nested ev_run's to stop iterating
ev_break (EV_A_ EVBREAK_ALL);
}
// another callback, this time for a time-out
static void
timeout_cb (EV_P_ ev_timer *w, int revents)
{
puts ("timeout");
// this causes the innermost ev_run to stop iterating
ev_break (EV_A_ EVBREAK_ONE);
}
int
main (void)
{
// use the default event loop unless you have special needs
struct ev_loop *loop = EV_DEFAULT;
// initialise an io watcher, then start it
// this one will watch for stdin to become readable
ev_io_init (&stdin_watcher, stdin_cb, /*STDIN_FILENO*/ 0, EV_READ);
ev_io_start (loop, &stdin_watcher);
// initialise a timer watcher, then start it
// simple non-repeating 5.5 second timeout
ev_timer_init (&timeout_watcher, timeout_cb, 5.5, 0.);
ev_timer_start (loop, &timeout_watcher);
// now wait for events to arrive
ev_run (loop, 0);
// break was called, so exit
return 0;
}Beberapa hal penting dari contoh ini:
- Semua callback watcher punya signature yang mirip: menerima loop, pointer ke watcher-nya sendiri, dan
revents(event apa yang terjadi). - Watcher one-shot (seperti
ev_iopada contoh ini) harus dihentikan manual di dalam callback-nya (ev_io_stop), jika tidak ingin dipanggil berulang kali. ev_break (loop, EVBREAK_ALL)menghentikan semuaev_runyang bersarang (nested), sedangkanev_break (loop, EVBREAK_ONE)hanya menghentikanev_runyang paling dalam (innermost) saja.- Program akan blok di
ev_run(loop, 0)sampai salah satu watcher trigger dan memanggilev_break.
libuv: Abstraksi Cross-Platform untuk Node.js
Karena libev hanya berjalan optimal di dunia Unix/Linux (berbasis epoll/kqueue), Node.js menggunakan libuv sebagai lapisan abstraksi yang menyatukan mekanisme event loop lintas OS. Secara arsitektur, libuv menyediakan Network I/O (TCP, UDP, TTY, Pipe, dst) serta File I/O, DNS Ops, dan ruang untuk user code, yang di baliknya dipetakan ke mekanisme native OS: epoll/kqueue/event ports di Unix-like, IOCP di Windows, dan Thread Pool untuk operasi yang secara native bersifat blocking (misalnya file I/O).

Satu iterasi loop pada libuv mengikuti urutan tahap yang tetap: memperbarui waktu loop, mengecek apakah loop masih “alive” (ada watcher aktif), menjalankan timer yang sudah jatuh tempo, memanggil pending callback, menjalankan idle handle, menjalankan prepare handle, polling untuk I/O, menjalankan check handle, lalu memanggil close callback — sebelum kembali ke awal.

Struktur iterasi ini pada dasarnya adalah generalisasi dari pola yang sama seperti pada libev: event loop terus berputar, mengecek berbagai sumber event (timer, I/O, callback tertunda), dan memanggil handler-nya masing-masing pada fase yang sesuai.
Asynchronous Programming di Python: dari Threading ke Asyncio
Masalah Threading pada Python
Sebelum masuk ke asyncio, penting melihat dulu kenapa pendekatan thread-per-task bermasalah untuk skenario I/O bound dengan concurrency tinggi. Contoh berikut membuat 10.000 thread yang masing-masing hanya sleep(60):
import os
from time import sleep
from threading import Thread
threads = [
Thread(target=lambda: sleep(60)) for i in range(10000)
]
[t.start() for t in threads]
print(f'PID = {os.getpid()}')
[t.join() for t in threads]Meskipun setiap thread tidak melakukan komputasi berat (hanya tidur), proses Python ini menghabiskan sekitar 82 GB memory — karena setiap thread mendapat alokasi stack tersendiri dari OS. Ini menjadi bukti konkret dari motivasi di awal catatan ini: model satu-thread-per-request tidak scalable untuk menangani banyak koneksi/task I/O bound secara bersamaan.

Sebagai perbandingan, pola thread lain yang lebih terkontrol adalah menggunakan thread pool lewat concurrent.futures.ThreadPoolExecutor, yang membatasi jumlah thread aktif sekaligus (max_workers) alih-alih membuat satu thread per task:
from concurrent.futures import ThreadPoolExecutor as Executor
def worker(data):
<process the data>
with Executor(max_workers=10) as exe:
future = exe.submit(worker, data)Namun thread pool tetap memakai thread OS sungguhan (dengan overhead context switching dan risiko race condition), sehingga untuk beban I/O bound berskala besar, pendekatan event loop asynchronous (asyncio) menjadi alternatif yang lebih efisien dalam penggunaan resource.
Asyncio: Event Loop dengan async/await
Asyncio adalah library Python untuk concurrent programming menggunakan syntax async/await. Cara kerjanya sejalan dengan arsitektur event loop yang dibahas di awal:
- Menjalankan event loop.
- Task dieksekusi sesuai event yang terjadi.
- Task yield secara voluntary (menyerahkan kendali kembali ke event loop lewat
await), bukan di-preempt paksa seperti pada thread OS.
Alur penggunaan asyncio secara umum:
- Menjalankan event loop.
- Memanggil fungsi async/await.
- Membuat sebuah task untuk berjalan pada loop.
- Menunggu task selesai.
- Menutup (closing) loop.
Contoh paling sederhana (“Hello World”):
import asyncio
async def main():
print('Hello ...')
await asyncio.sleep(1)
print('... World!')
# Python 3.7+
asyncio.run(main())Contoh yang lebih lengkap, memperlihatkan pengelolaan loop secara eksplisit — membuat task, menunggunya selesai, membatalkan task-task yang masih pending, lalu menutup loop:
import asyncio
import time
async def main():
print(f"{time.ctime()} Hello!")
await asyncio.sleep(1.0)
print(f"{time.ctime()} Goodbye!")
loop = asyncio.get_event_loop()
task = loop.create_task(main())
loop.run_until_complete(task)
pending = asyncio.all_tasks(loop=loop)
for task in pending:
task.cancel()
group = asyncio.gather(*pending, return_exceptions=True)
loop.run_until_complete(group)
loop.close()Menangani Blocking Call di Asyncio: run_in_executor
Sesuai prinsip arsitektur event loop — handler tidak boleh melakukan operasi blocking — sebuah fungsi yang secara inheren blocking (misalnya memanggil time.sleep() langsung, bukan await asyncio.sleep()) tidak boleh dipanggil langsung di dalam coroutine, karena akan memblokir seluruh event loop dan menghentikan semua task lain yang sedang berjalan. Solusinya adalah melempar (offload) pemanggilan fungsi blocking tersebut ke executor (thread pool) lewat loop.run_in_executor, supaya event loop tetap bisa lanjut memproses task lain sementara fungsi blocking itu berjalan di thread terpisah:
# quickstart_exe.py
import time
import asyncio
async def main():
print(f'{time.ctime()} Hello!')
await asyncio.sleep(1.0)
print(f'{time.ctime()} Goodbye!')
def blocking():
time.sleep(0.5)
print(f"{time.ctime()} Hello from a thread!")
loop = asyncio.get_event_loop()
task = loop.create_task(main())
loop.run_in_executor(None, blocking)
loop.run_until_complete(task)
pending = asyncio.all_tasks(loop=loop)
for task in pending:
task.cancel()
group = asyncio.gather(*pending, return_exceptions=True)
loop.run_until_complete(group)
loop.close()Ini adalah jawaban konkret asyncio untuk poin “bagaimana menangani blocking request” pada arsitektur library asynchronous: blocking call tetap boleh ada, tapi dieksekusi di luar event loop (di thread pool), bukan langsung di dalam coroutine/handler.
Coroutines
Coroutine adalah objek yang mengenkapsulasi kemampuan untuk melanjutkan (resume) eksekusi sebuah fungsi yang sudah di-suspend sebelum selesai. Inilah mekanisme dasar yang membuat async def + await bisa “berhenti sejenak” lalu lanjut lagi tanpa memblokir thread.
Eksekusi sebuah coroutine dapat dipicu dengan salah satu dari tiga cara:
- Memanggil
coro.send(None)secara langsung. loop.create_task(coro)— mendaftarkan coroutine sebagai task pada event loop.await coro— menunggu coroutine lain selesai dari dalam coroutine yang sedang berjalan.
async def f():
await asyncio.sleep(1.0)
return 123
async def main():
result = await f()
return resultPada contoh ini, main() men-await coroutine f(); eksekusi main() akan di-suspend sampai f() selesai (termasuk saat f() sendiri men-await asyncio.sleep(1.0)), lalu dilanjutkan kembali dengan nilai result yang dikembalikan f() — semua ini terjadi dalam satu thread, dikoordinasikan oleh event loop.
Flashcard
flashcards Mengapa asynchronous/event-based programming bisa lebih efisien dibanding thread-per-request untuk aplikasi I/O bound? :: Karena tidak perlu alokasi stack tersendiri untuk tiap thread, tidak ada context switching saat penanganan resource, dan menghindari race condition pada multitasking thread. Apa dua kesalahpahaman umum tentang asynchronous programming yang perlu diluruskan? :: Asynchronous tidak membuat program menjadi lebih cepat, dan asynchronous tidak berarti tidak perlu menggunakan thread lagi. Bagaimana arsitektur dasar sebuah library asynchronous menangani blocking request? :: Blocking request diubah menjadi pola event loop + handler: event loop mengecek event yang terjadi dan memanggil handler yang sesuai; handler itu sendiri tidak boleh melakukan operasi blocking dan harus selesai secepat mungkin. Apa itu watcher pada libev, dan berikan tiga contoh jenisnya. :: Watcher adalah struktur yang dialokasikan untuk menyatakan minat terhadap sebuah event tertentu. Contoh jenisnya: ev_io (I/O readiness, level trigger), ev_timer (timeout), ev_signal (signal OS). Apa perbedaan efek ev_break(loop, EVBREAK_ALL) dan ev_break(loop, EVBREAK_ONE)? :: EVBREAK_ALL menghentikan semua ev_run yang bersarang (nested), sedangkan EVBREAK_ONE hanya menghentikan ev_run yang paling dalam (innermost). Apa peran libuv terhadap libev, dan mengapa Node.js membutuhkannya? :: libuv adalah lapisan abstraksi di atas libev pada Linux dan IOCP pada Windows, sehingga Node.js bisa memakai satu API event loop yang portable lintas platform, bukan hanya di sistem berbasis epoll/kqueue. Mengapa membuat 10.000 thread Python (masing-masing hanya sleep) bisa menghabiskan puluhan GB memory? :: Karena setiap thread OS mendapat alokasi stack tersendiri, sehingga jumlah thread yang sangat banyak membebani memory meski tiap thread tidak melakukan komputasi berat — inilah motivasi utama beralih ke event loop asynchronous seperti asyncio. Bagaimana cara menangani sebuah fungsi yang inheren blocking (misalnya time.sleep) di dalam program asyncio agar tidak memblokir event loop? :: Lempar (offload) fungsi blocking tersebut ke thread pool lewat loop.run_in_executor(None, fungsi_blocking), sehingga event loop tetap bisa lanjut memproses task lain sementara fungsi itu berjalan di thread terpisah. Apa itu coroutine, dan sebutkan tiga cara untuk mengeksekusinya. :: Coroutine adalah objek yang mengenkapsulasi kemampuan untuk melanjutkan (resume) eksekusi fungsi yang telah di-suspend sebelum selesai. Cara eksekusinya: memanggil coro.send(None), loop.create_task(coro), atau await coro.