Berpindah dari JavaScript ke Python terasa seperti pindah ke kota dengan rambu jalan yang sama tetapi hukum lalu lintas yang berbeda. Sintaksnya terlihat ramah dan akrab. Anda melihat async dan await ada di sana dalam tata bahasanya, sehingga Anda berasumsi model mentalnya akan berpindah dengan mulus. Ternyata tidak. Satu pola kebiasaan dari JavaScript secara diam-diam merusak performa Python Anda tanpa menyebabkan crash, tanpa mencatat error, dan tanpa muncul dalam tinjauan kode (code review) singkat.
Bagaimana JavaScript Mengajarkan Anda untuk Memulai dan Melupakan
Dalam JavaScript, pemanggilan fungsi async bersifat eager. Saat Anda memanggilnya, engine langsung membuat sebuah Promise dan pekerjaan pun segera dimulai. Event loop sudah langsung beraksi. Itulah sebabnya pengembang JavaScript secara alami menulis kode seperti ini:
const userPromise = fetchUser(id);
const ordersPromise = fetchOrders(id);
const user = await userPromise;
const orders = await ordersPromise;
Kedua permintaan jaringan sudah berjalan sebelum salah satu await tercapai. await pertama menangguhkan fungsi saat ini sampai fetchUser selesai (resolves), tetapi fetchOrders sudah berjalan di latar belakang sejak baris sebelumnya. Pada saat Anda membutuhkan variabel orders, permintaan kedua mungkin sudah selesai. Pola ini terasa sangat alami di JavaScript sehingga banyak pengembang bahkan tidak menganggapnya sebagai trik konkurensi. Ini hanyalah cara kerja async.
Kejutan Python: Coroutine yang Dingin
Python menggunakan kontrak yang berbeda. Saat Anda memanggil fungsi async def di Python, Anda tidak memulai pekerjaan apa pun. Anda menerima sebuah objek coroutine. Bayangkan itu sebagai sebuah resep yang tertulis di atas kertas. Bahan-bahannya terdaftar, langkah-langkahnya jelas, tetapi tidak ada yang sedang dimasak di dalam oven. Sampai ada sesuatu yang secara eksplisit menjalankan coroutine tersebut melalui event loop, ia akan tetap tidak aktif.
Inilah jebakannya. Seorang insinyur JavaScript yang membutuhkan data pengguna dan pesanan mereka mungkin akan menulis ini di Python:
user_coro = fetch_user(id)
orders_coro = fetch_orders(id)
user = await user_coro
orders = await orders_coro
Terlihat konkuren. Terasa konkuren. Namun sebenarnya sepenuhnya sekuensial.
Baris pertama menetapkan coroutine yang tidak aktif ke user_coro. Baris kedua menetapkan coroutine tidak aktif lainnya ke orders_coro. Ketika eksekusi mencapai await user_coro, Python akhirnya memulai tugas pertama dan menjalankannya hingga selesai. Hanya setelah fetch_user selesai, interpreter mencapai await orders_coro dan memulai tugas kedua. Total waktu eksekusi Anda adalah jumlah dari kedua operasi I/O, bukan yang terlama. Anda tidak menjalankannya secara paralel. Anda menjalankannya satu per satu dengan langkah tambahan.
Mengapa Bug Ini Tidak Terlihat
Ini adalah jenis regresi performa yang bisa bertahan selama berbulan-bulan. Kodenya adalah Python yang valid. Lolos dari type checker. Mengembalikan hasil yang benar. Ia hanya berjalan dengan kecepatan setengah, atau bahkan lebih lambat. Karena tidak ada stack trace dan tidak ada peringatan, tim engineering sering kali mencari ke tempat lain terlebih dahulu. Mereka menambahkan cache Redis, meningkatkan tier database, atau berpindah ke wilayah hosting yang berbeda. Pelaku sebenarnya adalah ketidakcocokan halus dalam ekspektasi tentang apa yang sebenarnya dilakukan oleh await.
Tiga Cara untuk Membuat Python Benar-benar Menjalankan Sesuatu secara Konkuren
Untuk memperbaikinya, Anda harus memberi tahu event loop Python untuk menjadwalkan pekerjaan tersebut segera. Anda membutuhkan sesuatu yang lebih aktif daripada sekadar coroutine mentah. Anda membutuhkan sebuah Task.
1. asyncio.create_task
Terjemahan paling langsung dari pola JavaScript adalah membungkus coroutine Anda dalam sebuah Task. Sebuah Task dijadwalkan pada event loop segera setelah Anda membuatnya. Ini adalah padanan Python terdekat untuk Promise JavaScript yang sedang berjalan.
user_task = asyncio.create_task(fetch_user(id))
orders_task = asyncio.create_task(fetch_orders(id))
user = await user_task
orders = await_orders_task
Sekarang baik fetch_user maupun fetch_orders sudah berjalan sebelum await pertama. Ketika Anda mencapai await user_task, Anda hanya berhenti sampai Task spesifik tersebut selesai, tetapi Task lainnya tetap berjalan. Jika fetch_orders selesai lebih dulu, hasilnya akan menunggu di dalam orders_task sampai Anda memintanya.
Namun, berhati-hatilah. Jika Anda membuat sebuah Task dan tidak pernah melakukan await padanya, Python akan mengeluarkan error tentang destroyed pending task. Anda tetap harus mengumpulkan hasil Anda.
2. asyncio.gather
Jika Anda memiliki beberapa coroutine yang semuanya harus selesai sebelum Anda melanjutkan, asyncio.gather menangani proses boilerplate untuk Anda. Ia menjadwalkan setiap coroutine sebagai Task secara internal dan menunggu mereka secara bersamaan.
user, orders = await asyncio.gather(fetch_user(id), fetch_orders(id))
Ini ringkas dan mudah dibaca. Sangat berguna ketika operasi-operasi tersebut bersifat independen dan Anda menginginkan satu baris kode yang menyatakan "jalankan semua ini, lalu berikan saya setiap hasilnya." Ini juga mempertahankan urutan argumen dalam list atau tuple yang dikembalikan, meskipun task yang mendasarinya selesai dalam urutan yang berbeda.
3. asyncio.TaskGroup
Python 3.11 memperkenalkan TaskGroup, yang menghadirkan structured concurrency ke dalam pustaka standar. Alih-alih membuat task secara manual, Anda menggunakan context manager yang memastikan setiap task yang dibuat selesai dengan benar. Jika satu task memicu pengecualian (exception), task lainnya akan dibatalkan secara otomatis.
async with asyncio.TaskGroup() as tg:
user_task = tg.create_task(fetch_user(id))
orders_task = tg.create_task(fetch_orders(id))
user = user_task.result()
orders = orders_task.result()
Pola ini sangat bagus untuk alur kerja (workflow) yang kompleks. Ini menghilangkan risiko adanya Task yang terabaikan (orphaned), dan mengelompokkan siklus hidup operasi terkait di bawah satu payung logis. Jika basis kode Anda berjalan pada Python 3.11 atau yang lebih baru, ini sering kali merupakan arsitektur terbersih untuk fan-out concurrency.
Model Mental: await Berarti "Jalankan Ini Sekarang"
Pelajaran utamanya bersifat linguistik. Di JavaScript, Anda dapat membaca await sebagai "sementara itu." Anda memulai pekerjaan, melakukan hal lain, dan baru berhenti sejenak saat Anda membutuhkan nilainya. Di Python, await berarti "jalankan coroutine ini hingga titik suspensi berikutnya atau hingga selesai." Jika coroutine belum dijadwalkan, await adalah yang menjadwalkannya. Itulah sebabnya Anda tidak bisa memulai dua coroutine mentah lalu melakukan await pada keduanya nanti. Anda belum memberikan tugas apa pun kepada event loop untuk dilakukan sementara itu.
Bayangkan coroutine Python seperti fungsi generator. Memanggil generator tidak langsung mengiterasinya. Anda perlu melakukan iterasi padanya, memanggil next(), atau menyerahkannya ke sebuah consumer. Async bekerja dengan cara yang sama. asyncio.create_task adalah consumer yang mengatakan "masukkan ini ke dalam event loop sekarang juga." await berikutnya hanya menunggu sinyal penyelesaian.
Satu kebiasaan konkret yang membantu: kapan pun Anda menetapkan pemanggilan fungsi async ke sebuah variabel tanpa await, tanyakan pada diri sendiri apakah Anda sudah menjadwalkannya. Jika sisi kanan tidak dibungkus dalam create_task, gather, atau TaskGroup, maka ia tidak sedang berjalan. Itu hanyalah sebuah resep yang tergeletak di atas meja.
Kesimpulan
Runtime async Python sangat kuat, tetapi menuntut niat yang eksplisit. Bahasa ini tidak memulai pekerjaan latar belakang hanya karena Anda memanggil sebuah fungsi. Jika Anda berasal dari JavaScript, audit setiap tempat di mana Anda menyimpan coroutine dalam sebuah variabel dan melakukan await nanti. Kecuali jika Anda mempromosikannya menjadi sebuah Task terlebih dahulu, Anda sebenarnya menulis kode sekuensial yang berpakaian async. Mulailah pekerjaan dengan sebuah Task, lalu tunggu hasilnya. Begitulah cara Anda mengubah async Python dari hambatan (bottleneck) yang senyap menjadi alat konkurensi yang sesungguhnya.
