Asyncio

Pemrograman async dengan async/await, event loop, task, dan gather.

Daftar isi

asyncio menjalankan konkurensi pada satu thread dengan gaya pemrograman async, yaitu fungsi yang sedang menunggu operasi lambat menyerahkan kontrol agar fungsi lain dapat berjalan. Pustaka ini menyediakan event loop, korutin (coroutine), dan task sebagai pengganti thread untuk beban yang sebagian waktunya menunggu I/O. Topik ini membahas penulisan korutin dengan async dan await, kerja event loop, pengumpulan hasil melalui gather, pembuatan task dengan create_task, beserta kesalahan umumnya.

Korutin dan asyncio.run#

Fungsi yang didefinisikan dengan async def disebut korutin, yaitu fungsi yang eksekusinya dapat dihentikan sementara pada setiap await lalu dilanjutkan kembali. Memanggil fungsi async tidak langsung menjalankannya, melainkan menghasilkan objek korutin, sehingga objek itu harus dijalankan oleh event loop. Fungsi asyncio.run menyiapkan loop, menjalankan korutin utama sampai selesai, lalu menutup loopnya.

python Contoh: korutin pertama
import asyncio

async def tugas(nama, durasi):
    print(f"{nama} mulai")
    await asyncio.sleep(durasi)
    print(f"{nama} selesai")
    return nama.upper()

async def utama():
    hasil = await tugas("A", 0.02)
    print("hasil:", hasil)

asyncio.run(utama())
text Keluaran
A mulai
A selesai
hasil: A

Pernyataan await asyncio.sleep(durasi) adalah bentuk tunggu yang tidak memblokir, yaitu mengembalikan kontrol ke event loop selama durasinya berjalan. Pada contoh di atas belum ada korutin lain yang menunggu, sehingga urutannya terlihat seperti program biasa. Pergantian kontrol baru terlihat ketika lebih dari satu korutin dijalankan bersama.

Event loop dan titik tunggu#

Event loop adalah perulangan yang menjadwalkan korutin-korutin yang siap berjalan dan membangunkan kembali korutin yang tungguannya sudah selesai. Setiap await menjadi titik tunggu, yaitu tempat korutin menyerahkan kontrol dan loop memilih pekerjaan berikutnya. Satu thread cukup untuk ribuan tugas karena setiap tugas hanya berjalan pada saat benar-benar ada pekerjaan, bukan sambil menunggu.

Korutin AKorutin BKorutin CawaitlanjutEvent loopsumber I/O
Event loop menjadwalkan korutin pada satu thread dan membangunkannya saat tungguannya selesai

Pengaruh titik tunggu terhadap durasi total dapat diukur dengan membandingkan dua korutin yang dijalankan berurutan dan bersamaan.

python Contoh: durasi berurutan dibanding bersamaan
import asyncio
import time

async def muat():
    await asyncio.sleep(0.1)
    return 1

async def berurutan():
    awal = time.perf_counter()
    await muat()
    await muat()
    return time.perf_counter() - awal

async def bersamaan():
    awal = time.perf_counter()
    await asyncio.gather(muat(), muat())
    return time.perf_counter() - awal

async def utama():
    d1 = await berurutan()
    d2 = await bersamaan()
    print(d1 > d2)

asyncio.run(utama())
text Keluaran
True

Versi berurutan memakan waktu sekitar dua kali lipat versi bersamaan karena tungguan keduanya tidak tumpang-tindih, sedangkan gather menjalankan keduanya sekaligus sehingga durasinya mendekati satu tungguan saja. Angka True menegaskan perbandingan itu tanpa mencetak durasi persisnya, yang bergantung pada mesin. Pada tungguan I/O sungguhan seperti permintaan jaringan, selisih durasi antara kedua pola jauh lebih berarti daripada pada contoh sleep di atas.

gather dan urutan hasil#

Fungsi asyncio.gather menjalankan beberapa korutin sekaligus dan mengembalikan seluruh hasilnya setelah semuanya selesai. Hasil selalu berurutan sesuai urutan argumen, meskipun korutin yang diberikan selesai dalam urutan yang berbeda. Apabila salah satu korutin menimbulkan error, error itu dilempar ulang pada await gather-nya, dan argumen return_exceptions=True tersedia bila error ingin dikumpulkan sebagai nilai.

python Contoh: gather menjaga urutan hasil
import asyncio

async def muat(nama, durasi):
    await asyncio.sleep(durasi)
    return f"{nama} selesai"

async def utama():
    hasil = await asyncio.gather(
        muat("satu", 0.03),
        muat("dua", 0.02),
        muat("tiga", 0.01),
    )
    print(hasil)

asyncio.run(utama())
text Keluaran
['satu selesai', 'dua selesai', 'tiga selesai']

Korutin tiga selesai paling dulu karena durasinya paling pendek, tetapi daftar hasilnya tetap dimulai dari satu sesuai urutan argumen. Sifat ini membuat gather aman dipakai untuk memetakan daftar masukan ke daftar hasil tanpa pengurutan ulang. Perilakunya sejajar dengan executor.map pada concurrent.futures.

Task dan create_task#

Fungsi asyncio.create_task membungkus korutin menjadi objek Task dan segera menjadwalkannya pada event loop, tanpa menunggu await. Korutin itu pun berjalan di latar sementara korutin pemanggilnya melakukan hal lain, dan hasilnya dapat ditunggu belakangan. Objek Task yang sama juga dapat diteruskan ke gather.

python Contoh: create_task dan gather
import asyncio

async def muat(nama, durasi):
    await asyncio.sleep(durasi)
    return nama

async def utama():
    t1 = asyncio.create_task(muat("A", 0.02), name="muat-A")
    t2 = asyncio.create_task(muat("B", 0.01), name="muat-B")
    print("tugas dibuat")
    hasil = await asyncio.gather(t1, t2)
    print(hasil)

asyncio.run(utama())
text Keluaran
tugas dibuat
['A', 'B']

Cetakan tugas dibuat muncul lebih dahulu karena kedua task baru dijadwalkan, sedangkan korutin utama masih berjalan hingga menemui await gather. Pada titik itu kontrol berpindah, kedua task dijalankan, dan hasilnya kembali dalam urutan argumen. Jalankan demo berikut, tekan tombol Jalankan berurutan lalu tombol Jalankan bersamaan, kemudian bandingkan pola baris log beserta total waktunya.

Demo tersebut meniru pola yang sama dalam JavaScript yang juga berjalan satu thread, yaitu await pada setiap langkah menjadi titik tunggu dan tugas lain dijalankan selama waktu tunggu itu. Versi bersamaan menyelesaikan kedua tugas dengan total waktu mendekati tungguan terlama, sedangkan versi berurutan menjumlahkan seluruh tungguan. Perilaku itu persis seperti perbandingan gather pada contoh Python di atas.

Kesalahan umum#

Kesalahan pertama adalah memanggil fungsi async tanpa await. Pemanggilan itu hanya menghasilkan objek korutin tanpa menjalankan isinya, sehingga pekerjaannya tidak pernah terjadi.

python Contoh: objek korutin sebelum dijalankan
import asyncio

async def sapa():
    return "halo"

async def utama():
    korutin = sapa()
    print(type(korutin).__name__)
    print(await korutin)

asyncio.run(utama())
text Keluaran
coroutine
halo

Cetakan coroutine menunjukkan bahwa pemanggilan sapa() baru menghasilkan objek, dan nilainya baru diperoleh setelah await. Apabila objek korutin dibuang tanpa pernah dijalankan, Python memunculkan peringatan RuntimeWarning: coroutine was never awaited yang menandai pekerjaan yang tidak pernah dijalankan.

Kesalahan kedua adalah memanggil fungsi yang memblokir, seperti time.sleep, di dalam korutin. Event loop tidak dapat berpindah ke korutin lain selama pemanggilan itu berlangsung, sehingga tidak ada task lain yang berjalan.

python Contoh: time.sleep memblokir event loop
import asyncio
import time

async def tugas(nama):
    print(f"{nama} mulai")
    await asyncio.sleep(0.02)
    print(f"{nama} selesai")

async def utama():
    t = asyncio.create_task(tugas("B"))
    time.sleep(0.05)
    print("utama kembali")
    await t

asyncio.run(utama())
text Keluaran
utama kembali
B mulai
B selesai

Task B bahkan belum mulai ketika time.sleep(0.05) berlangsung, karena korutin utama tidak pernah menyerahkan kontrol dan event loop tidak pernah berjalan kembali. Cetakan utama kembali mendahului B mulai sebagai bukti pemblokiran itu. Penggantinya adalah await asyncio.sleep() untuk tungguan, atau asyncio.to_thread() untuk fungsi pustaka yang memblokir, sehingga pemanggilannya berpindah ke thread lain.

Ringkasan#

  • Fungsi async def menghasilkan korutin yang baru berjalan ketika dijalankan oleh event loop, misalnya melalui asyncio.run.
  • Setiap await menjadi titik tunggu yang menyerahkan kontrol, sehingga beberapa korutin dapat berjalan tumpang-tindih pada satu thread.
  • asyncio.gather mengembalikan hasil berurutan sesuai argumennya, dan asyncio.create_task menjadwalkan korutin segera sebagai objek Task.
  • Pemanggilan fungsi yang memblokir membekukan seluruh loop, sehingga tungguan harus melalui await asyncio.sleep atau asyncio.to_thread.

Referensi#