← Semua pembelajaran / Python untuk AI Engineer
Fase 3 Β· Async & HTTP

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:

ConcurrencyParallelism
ArtinyaBanyak tugas sedang berlangsungBanyak tugas dieksekusi bersamaan
ButuhSatu inti CPU sudah cukupBeberapa inti CPU
AnalogiSatu koki mengurus 5 panci5 koki, masing-masing 1 panci
Di Pythonasyncio, threadingmultiprocessing

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

asynciothreading
Perpindahan tugasKooperatif β€” di titik awaitPreemptif β€” OS bisa memotong kapan saja
Berapa banyakPuluhan ribu taskRatusan thread (memori besar)
Biaya per unit~KB~MB (stack thread)
Race conditionJarang β€” titik pindah bisa diprediksiSering β€” bisa terpotong di mana saja
Perlu lock?JarangHampir selalu
Library harus mendukungYa β€” harus versi asyncTidak

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 kerjaPakaiContoh
I/O-bound, library async tersediaasyncioPanggilan LLM, HTTP, DB async
I/O-bound, hanya ada library sinkronasyncio.to_threadSDK lama, driver sinkron
CPU-boundProcessPoolExecutorNumerik berat, pemrosesan gambar
Campuranasyncio + process poolServer 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

  1. Satu thread, satu loop. Tidak ada eksekusi paralel sungguhan.
  2. await = "aku menunggu, jalankan yang lain".
  3. Tanpa await = tidak ada yang bisa jalan. Kode blocking membekukan semuanya.
  4. Percepatan datang dari menunggu bersamaan, bukan dari menghitung lebih cepat.
  5. 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.