Coroutine dan event loop adalah mesinnya; modul asyncio di standard library adalah panel kendalinya. Modul ini menyediakan function untuk menunggu, menjalankan banyak coroutine sekaligus, membatasi waktu tunggu, memanggil kode synchronous, sampai antrean antar-task. Chapter ini memandu kita menyusuri fasilitas yang paling sering dipakai sehari-hari, lengkap dengan contoh yang bisa dijalankan langsung.

Program Async Pertama #

Semua program asyncio modern berawal dari pola yang sama: satu coroutine main sebagai pintu masuk, dijalankan lewat asyncio.run():

python
import asyncio

async def main():
    print("Halo, asyncio!")

asyncio.run(main())

Output yang Dihasilkan:

text
Halo, asyncio!

Sederhana, tetapi di baliknya asyncio.run() melakukan tiga pekerjaan: membuat event loop, menjadwalkan main(), dan menutup loop setelah main() selesai. Siklus kerja loop itu sendiri bisa digambarkan begini:

Loop terus berputar memilih task yang siap sampai tidak ada lagi yang menunggu. Semua fasilitas di bawah ini bekerja di dalam siklus tersebut.

sleep, gather, dan Hasil yang Terurut #

asyncio.sleep() adalah pengganti async untuk time.sleep(): ia menyerahkan kontrol ke event loop selama jeda yang diminta. Untuk mengukur durasi, event loop menyediakan loop.time() — jam monotonic yang juga dipakai internal asyncio:

python
import asyncio

async def main():
    loop = asyncio.get_running_loop()
    mulai = loop.time()
    await asyncio.sleep(1)
    print(f"tercatat {loop.time() - mulai:.1f} detik")

asyncio.run(main())

Output yang Dihasilkan:

text
tercatat 1.0 detik

Untuk menunggu banyak coroutine sekaligus, asyncio.gather() menerima sejumlah coroutine dan mengembalikan daftar hasil sesuai urutan argumen, bukan urutan selesai:

python
import asyncio

async def ambil_data(nama, detik):
    await asyncio.sleep(detik)
    return f"{nama} siap"

async def main():
    hasil = await asyncio.gather(
        ambil_data("Budi", 2),
        ambil_data("Siti", 1),
        ambil_data("Ani", 3),
    )
    for baris in hasil:
        print(baris)

asyncio.run(main())

Output yang Dihasilkan:

text
Budi siap
Siti siap
Ani siap

Siti selesai lebih dulu, tetapi hasilnya tetap berada di posisi kedua karena posisi itulah yang dipakai gather saat menyusun daftar hasil. Perilaku ini membuat pemrosesan hasil tetap mudah diprediksi. Dua catatan pelengkap: asyncio.sleep(0) adalah jeda nol detik yang berguna hanya untuk menyerahkan kontrol sesaat ke task lain, dan gather menerima opsi return_exceptions=True jika kita ingin kegagalan satu task tidak langsung menggugurkan task lainnya.

Jangan Blokir Event Loop #

Aturan paling penting asyncio: coroutine hanya boleh menunggu lewat await, bukan lewat pemanggilan blocking. Bandingkan dua cara “menunggu” berikut:

python
import asyncio
import time

async def detak():
    mulai = time.perf_counter()
    for i in range(3):
        await asyncio.sleep(0.2)
        print(f"detak {i} pada {time.perf_counter() - mulai:.1f} detik")

async def tugas_pemblokir():
    print("tugas pemblokir mulai")
    time.sleep(0.5)
    print("tugas pemblokir selesai")

async def main():
    await asyncio.gather(detak(), tugas_pemblokir())

asyncio.run(main())

Output yang Dihasilkan (angka detik dapat sedikit berbeda di mesin Anda):

text
tugas pemblokir mulai
tugas pemblokir selesai
detak 0 pada 0.5 detik
detak 1 pada 0.7 detik
detak 2 pada 0.9 detik

detak seharusnya berdenyut tiap 0.2 detik, tetapi detak 0 baru muncul pada 0.5 detik — tepat setelah time.sleep(0.5) selesai. Selama tugas_pemblokir tidur secara blocking, satu-satunya thread dimiliki habis oleh time.sleep, dan event loop tidak bisa menjalankan apa pun.

Timeout dengan wait_for dan asyncio.timeout #

Operasi jaringan bisa menggantung tanpa batas. Dua fasilitas asyncio membatasinya. Yang pertama, asyncio.wait_for(), membatalkan coroutine yang melebihi batas waktu lalu menaikkan TimeoutError:

python
import asyncio

async def unduh_lambat():
    await asyncio.sleep(10)
    return "berhasil"

async def main():
    try:
        await asyncio.wait_for(unduh_lambat(), timeout=1)
    except TimeoutError:
        print("unduh_lambat dibatalkan setelah 1 detik")

asyncio.run(main())

Output yang Dihasilkan:

text
unduh_lambat dibatalkan setelah 1 detik

Yang kedua, context manager asyncio.timeout() (hadir di Python 3.11), membatasi seluruh blok async with sekaligus — lebih rapi saat beberapa operasi berbagi satu batas waktu:

python
import asyncio

async def unduh_lambat():
    await asyncio.sleep(10)
    return "berhasil"

async def main():
    try:
        async with asyncio.timeout(1):
            await unduh_lambat()
    except TimeoutError:
        print("asyncio.timeout membatalkan unduh_lambat")

asyncio.run(main())

Output yang Dihasilkan:

text
asyncio.timeout membatalkan unduh_lambat

Keduanya sama-sama membatalkan operasi yang terlalu lama; mekanisme pembatalannya kita bedah pada Chapter Cancellation dan Timeouts.

to_thread dan Queue #

Bagaimana memanggil library synchronous dari dunia async? asyncio.to_thread() menjalankan function biasa di thread terpisah dan mengembalikan coroutine yang bisa di-await, sehingga event loop tetap bebas bergerak:

python
import asyncio
import time

def baca_konfigurasi():
    time.sleep(1)
    return "konfigurasi siap"

async def detak():
    for i in range(3):
        await asyncio.sleep(0.25)
        print(f"detak {i}")

async def main():
    hasil, _ = await asyncio.gather(
        asyncio.to_thread(baca_konfigurasi),
        detak(),
    )
    print(hasil)

asyncio.run(main())

Output yang Dihasilkan:

text
detak 0
detak 1
detak 2
konfigurasi siap

Terakhir, asyncio.Queue() adalah antrean aman-async untuk memindahkan data antar-task — versi asyncio dari pola producer-consumer:

python
import asyncio

async def produsen(antrian):
    for item in ["kopi", "teh", "susu"]:
        await antrian.put(item)

async def konsumen(antrian):
    while True:
        item = await antrian.get()
        if item is None:
            break
        print(f"memproses {item}")
        await asyncio.sleep(0.1)
    print("konsumen berhenti")

async def main():
    antrian = asyncio.Queue()
    tugas_produsen = asyncio.create_task(produsen(antrian))
    tugas_konsumen = asyncio.create_task(konsumen(antrian))
    await tugas_produsen
    await antrian.put(None)
    await tugas_konsumen

asyncio.run(main())

Output yang Dihasilkan:

text
memproses kopi
memproses teh
memproses susu
konsumen berhenti

await antrian.get() menangguhkan konsumen sampai item tersedia, dan None dipakai sebagai sinyal berhenti. Perhatikan juga asyncio.create_task() yang muncul di sini — itulah alat utama chapter berikutnya untuk menjadwalkan coroutine sebagai task yang berjalan bersamaan.