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 await yang 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: requests yang blocking tidak berguna di dalam coroutine, dan harus diganti httpx atau aiohttp yang 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:

python
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):

text
urutan selesai: ['Ani', 'Siti', 'Budi']
total: 3.00 detik

Versi asyncio memakai asyncio.as_completed(), yang menyajikan coroutine sesuai urutan selesai:

python
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):

text
selesai: Ani
selesai: Siti
selesai: Budi
total: 3.00 detik

Untuk 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 #

PertanyaanPilihan 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():

python
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):

text
['sensor Budi terbaca', 'sensor Siti terbaca']
total: 1.00 detik

Dua 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.