Untuk pekerjaan I/O-bound, Python menawarkan dua dunia: threading yang kita pelajari pada Chapter Threading dan asyncio yang baru saja kita dalami. Keduanya sama-sama membuat program “menunggu banyak hal sekaligus”, tetapi bentuk kodenya, ekosistemnya, dan cara berpikirnya berbeda. Chapter penutup section ini merangkum kekuatan dan biaya masing-masing, memberi tabel keputusan, dan menunjukkan cara memakai keduanya sekaligus.
Kekuatan async #
Kekuatan utama async ada pada skala menunggu. Karena seluruh task berbagi satu thread, menangani 10.000 koneksi yang menganggur tidak butuh 10.000 thread — cukup 10.000 coroutine yang masing-masing hanya menyimpan status eksekusinya di memori (kilobyte, bukan megabyte). Dibanding thread, program async unggul pada:
- Banyaknya operasi I/O bersamaan — ribuan koneksi jaringan atau query dalam satu proses.
- Kemudahan berbagi state — karena pergantian task hanya terjadi di
awaityang bisa kita lihat, urutan akses ke variabel biasa jauh lebih mudah dinalar daripada race antar-thread yang bisa terjadi di baris mana pun. - Fitur bahasa yang lengkap — task group, timeout terstruktur, async iterator, hingga async context manager yang kita pelajari sepanjang section ini.
Biaya async: fungsi berwarna #
Biaya terbesar async sering disebut colored functions: begitu satu function menjadi async, semua pemanggilnya juga harus async (atau berhenti dan memindahkan hasil via asyncio.run). Satu titik await menular ke seluruh call stack di atasnya:
- Function helper, method class, hingga signature interface ikut berubah menjadi async saat menunggu hasil async.
- Refactoring program synchronous lama menjadi async biasanya menyentuh hampir seluruh lapisan kode.
- Ekosistem library ikut terbelah:
requestsyang blocking tidak berguna di dalam coroutine, dan harus digantihttpxatauaiohttpyang async-native.
Sementara itu, threading tetap sah dan sederhana: threading.Thread memanggil function biasa, library biasa tetap berfungsi, dan untuk beberapa operasi I/O yang jarang (unduh satu berkas sambil memproses satu lainnya), thread justru paling cepat ditulis. Chapter Memilih Threading atau Multiprocessing membahas sisi itu dari sudut pandang CPU-bound; di sini fokus kita tetap I/O-bound.
Demo yang Sama, Dua Dunia #
Mari kita jalankan tiga unduhan simulasi (durasi 1, 2, dan 3 detik) versi threading, lalu versi asyncio. Versi threading memakai threading.Thread dan threading.Lock untuk mengumpulkan hasil:
import threading
import time
hasil = []
kunci = threading.Lock()
def unduh(nama, durasi):
time.sleep(durasi)
with kunci:
hasil.append(nama)
mulai = time.perf_counter()
utas = [
threading.Thread(target=unduh, args=("Budi", 3)),
threading.Thread(target=unduh, args=("Siti", 2)),
threading.Thread(target=unduh, args=("Ani", 1)),
]
for utas_satu in utas:
utas_satu.start()
for utas_satu in utas:
utas_satu.join()
print("urutan selesai:", hasil)
print(f"total: {time.perf_counter() - mulai:.2f} detik")Output yang Dihasilkan (nilai total waktu dapat sedikit berbeda di mesin Anda):
urutan selesai: ['Ani', 'Siti', 'Budi']
total: 3.00 detikVersi asyncio memakai asyncio.as_completed(), yang menyajikan coroutine sesuai urutan selesai:
import asyncio
import time
async def unduh(nama, durasi):
await asyncio.sleep(durasi)
return nama
async def main():
mulai = time.perf_counter()
for tugas in asyncio.as_completed([unduh("Budi", 3), unduh("Siti", 2), unduh("Ani", 1)]):
nama = await tugas
print("selesai:", nama)
print(f"total: {time.perf_counter() - mulai:.2f} detik")
asyncio.run(main())Output yang Dihasilkan (nilai total waktu dapat sedikit berbeda di mesin Anda):
selesai: Ani
selesai: Siti
selesai: Budi
total: 3.00 detikUntuk tiga unduhan, keduanya sama-sama selesai dalam 3 detik — yang tercepat menentukan total. Perbedaan baru terasa pada skala: naikkan jumlah unduhan ke ratusan, dan versi threading harus memutar ratusan thread dengan biaya memori dan penjadwalan sistem operasi, sementara versi async hanya memutar ratusan coroutine di satu thread yang sama.
Tabel Keputusan #
| Pertanyaan | Pilihan yang cocok |
|---|---|
| Sedikit operasi I/O (di bawah belasan), kode sudah synchronous? | threading — paling sedikit perubahan |
| Banyak operasi I/O bersamaan (ratusan ke atas)? | asyncio — satu thread menangani semuanya |
| Server jaringan / web framework modern (FastAPI, aiohttp)? | asyncio — framework-nya sudah async-native |
| Library inti yang dibutuhkan hanya versi blocking? | threading, atau bungkus dengan to_thread |
| Beban berat berhitung (CPU-bound)? | multiprocessing — lihat Chapter Memilih Threading atau Multiprocessing |
Menggabungkan Keduanya dengan to_thread #
Dunia nyata sering berada di tengah: program async yang harus memanggil satu-dua function blocking (library lama, driver sensor, fungsi CPU-ringan yang tak bisa diubah). asyncio.to_thread() menjalankan function synchronous di thread bantu dan mengembalikannya sebagai coroutine yang bisa di-await — dan beberapa panggilan to_thread bisa berjalan bersamaan lewat gather():
import asyncio
import time
def baca_sensor(nama):
time.sleep(1)
return f"sensor {nama} terbaca"
async def main():
mulai = time.perf_counter()
hasil = await asyncio.gather(
asyncio.to_thread(baca_sensor, "Budi"),
asyncio.to_thread(baca_sensor, "Siti"),
)
print(hasil)
print(f"total: {time.perf_counter() - mulai:.2f} detik")
asyncio.run(main())Output yang Dihasilkan (nilai total waktu dapat sedikit berbeda di mesin Anda):
['sensor Budi terbaca', 'sensor Siti terbaca']
total: 1.00 detikDua panggilan blocking berjalan di dua thread bantu sekaligus, sementara event loop tetap bebas — dua “sensor” selesai dalam 1 detik, bukan 2.
Dari coroutine pertama sampai keputusan arsitektur, section ini menuntaskan fondasi asynchronous programming Python. Perjalanan berikutnya membawa kemampuan ini ke protokol dunia nyata pada Section Networking dan HTTP.