Python Asyncio: The Complete Guide
Model mental async: kenapa satu thread bisa menangani ribuan koneksi, dan bedanya dengan threading serta multiprocessing.
Intisari
- Async adalah concurrency (bergantian dengan cepat), bukan parallelism (benar-benar bersamaan).
- Satu thread, satu event loop. Task bergantian hanya di titik
awaitβ tidak pernah dipotong di tengah. - Karena perpindahan hanya di titik yang kamu tulis, kelas bug race condition khas threading hampir hilang.
- Async menang untuk I/O-bound. Untuk CPU-bound, ia justru memperlambat.
- GIL bukan masalah di async β karena memang tidak ada thread kedua yang memperebutkannya.
Concurrency vs parallelism
Dua kata ini sering dipakai bergantian, padahal artinya berbeda:
| Concurrency | Parallelism | |
|---|---|---|
| Artinya | Banyak tugas sedang berlangsung | Banyak tugas dieksekusi bersamaan |
| Butuh | Satu inti CPU sudah cukup | Beberapa inti CPU |
| Analogi | Satu koki mengurus 5 panci | 5 koki, masing-masing 1 panci |
| Di Python | asyncio, threading | multiprocessing |
Analogi koki itu cukup akurat. Saat air di panci 1 sedang mendidih, koki tidak berdiri diam
menungguinya β ia beralih memotong bawang untuk panci 2. await adalah momen
"panci ini sedang menunggu, kerjakan yang lain dulu".
Event loop
βββββββββββββββββββββββββββββββββββββββββββββββββ
β Event loop (satu thread) β
β β
β Antrean siap: [task A] [task C] β
β Menunggu I/O: [task B: jaringan] β
β [task D: jaringan] β
β β
β 1. Ambil task siap teratas β jalankan β
β 2. Task ketemu await β pindah ke "menunggu" β
β 3. I/O selesai β task kembali ke "siap" β
β 4. Ulangi β
βββββββββββββββββββββββββββββββββββββββββββββββββ
Konsekuensi penting: event loop hanya bisa berpindah task di titik
await. Kalau sebuah task berjalan tanpa await selama
30 detik, seluruh aplikasi diam selama 30 detik itu. Inilah alasan panggilan blocking begitu
merusak di kode async.
Async vs threading
| asyncio | threading | |
|---|---|---|
| Perpindahan tugas | Kooperatif β di titik await | Preemptif β OS bisa memotong kapan saja |
| Berapa banyak | Puluhan ribu task | Ratusan thread (memori besar) |
| Biaya per unit | ~KB | ~MB (stack thread) |
| Race condition | Jarang β titik pindah bisa diprediksi | Sering β bisa terpotong di mana saja |
| Perlu lock? | Jarang | Hampir selalu |
| Library harus mendukung | Ya β harus versi async | Tidak |
Keunggulan terbesar async yang jarang disebut: kodenya lebih mudah dinalar.
Di threading, tiap baris bisa dipotong OS di tengah β kamu harus mengasumsikan yang terburuk
di mana-mana. Di async, satu-satunya tempat task bisa berpindah adalah baris yang
kamu tulis await-nya. Kamu bisa membaca kode dan tahu persis di mana
interupsi mungkin terjadi.
GIL β dan kenapa ia tidak relevan di sini
Global Interpreter Lock membuat hanya satu thread yang bisa mengeksekusi bytecode Python pada
satu waktu. Itu alasan threading tidak mempercepat pekerjaan CPU-bound.
Di asyncio, pertanyaan itu tidak muncul: memang hanya ada satu thread. GIL tidak diperebutkan siapa pun. Percepatan datang bukan dari menjalankan lebih banyak kode, tapi dari tidak membuang waktu menunggu.
Peta keputusan
| Beban kerja | Pakai | Contoh |
|---|---|---|
| I/O-bound, library async tersedia | asyncio | Panggilan LLM, HTTP, DB async |
| I/O-bound, hanya ada library sinkron | asyncio.to_thread | SDK lama, driver sinkron |
| CPU-bound | ProcessPoolExecutor | Numerik berat, pemrosesan gambar |
| Campuran | asyncio + process pool | Server yang kadang menghitung berat |
Perbandingan konkret
import asyncio, time
import httpx
URLS = ["https://httpbin.org/delay/1"] * 10
# Sinkron: ~10 detik
def sinkron():
with httpx.Client() as c:
return [c.get(u).status_code for u in URLS]
# Async: ~1 detik
async def asinkron():
async with httpx.AsyncClient() as c:
r = await asyncio.gather(*(c.get(u) for u in URLS))
return [x.status_code for x in r]
t = time.perf_counter(); sinkron(); print(f"sinkron: {time.perf_counter()-t:.1f}s")
t = time.perf_counter(); asyncio.run(asinkron()); print(f"async: {time.perf_counter()-t:.1f}s")
Jalankan sendiri kode ini. Ini checkpoint Fase 3 dalam bentuk paling kecil β melihat selisihnya secara langsung jauh lebih berkesan daripada membaca penjelasannya.
βAsync menularβ
async def a(): await b()
async def b(): await c()
async def c(): await httpx_client.get(...)
Fungsi async hanya bisa di-await dari fungsi async lain. Jadi begitu satu fungsi
di bagian dalam jadi async, seluruh rantai pemanggilnya ikut jadi async sampai ke
asyncio.run() di titik paling luar. Ini kadang disebut "function coloring".
Dalam praktik, ini bukan masalah besar untuk aplikasi LLM β hampir semua yang kamu lakukan memang I/O, jadi memang layak async dari ujung ke ujung. Yang perlu kamu putuskan sekali di awal: apakah aplikasimu async atau tidak. Mencampur keduanya secara sembarangan justru merepotkan.
Model mental yang layak dibawa
- Satu thread, satu loop. Tidak ada eksekusi paralel sungguhan.
await= "aku menunggu, jalankan yang lain".- Tanpa
await= tidak ada yang bisa jalan. Kode blocking membekukan semuanya. - Percepatan datang dari menunggu bersamaan, bukan dari menghitung lebih cepat.
- Untuk CPU-bound, async tidak menolong β malah menambah beban.
Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.