23 poin oleh darjeeling 2025-11-16 | 12 komentar | Bagikan ke WhatsApp

Penyebab Perlambatan Kode Async dan Cara Mengatasinya (Ringkasan teknis)

Video ini membahas penyebab umum mengapa kode asyncio Python bisa lebih lambat daripada kode sinkron, serta metodologi teknis untuk mengatasinya.

1. Konsep inti Asyncio

  • Event Loop: Inti dari semua aplikasi asinkron. Dimulai dengan asyncio.run(), lalu mengelola dan menjadwalkan eksekusi task di satu thread.
  • Coroutine: Fungsi asinkron yang dideklarasikan dengan async def. Saat menemui kata kunci await, eksekusi dapat dijeda dan kontrol dikembalikan ke event loop.
  • Task: Membungkus coroutine dan menjadwalkannya agar berjalan secara bersamaan di event loop. Dibuat melalui asyncio.create_task().
  • Future: Objek level rendah yang merepresentasikan hasil akhir dari pekerjaan asinkron.

2. Contoh konversi kode sinkron ke asinkron

Gantilah time.sleep() sinkron yang ada dengan await asyncio.sleep() yang asinkron, deklarasikan fungsi dengan async def, dan jalankan coroutine utama dengan asyncio.run().


Kesalahan umum yang menyebabkan penurunan performa dan solusinya

Kesalahan 1: Eksekusi berurutan (Sequential Execution)

Jika task-task independen di-await secara berurutan alih-alih dijalankan paralel, total waktu eksekusi akan menjadi penjumlahan waktu semua task.

  • Contoh yang salah (berurutan):

    # Setiap await menunggu sampai pekerjaan sebelumnya selesai  
    await get_user_notifications()  
    await get_recent_activity()  
    await get_unread_messages()  
    
    Iklan
  • Solusi (paralel): Gunakan asyncio.gather atau asyncio.TaskGroup untuk menjalankan task-task independen secara bersamaan. Total waktu eksekusi dapat turun menjadi sebesar waktu task yang paling lama.

    # Tiga pekerjaan dimulai pada saat yang sama  
    await asyncio.gather(  
        get_user_notifications(),  
        get_recent_activity(),  
        get_unread_messages()  
    )  
    

Perbandingan alat eksekusi paralel

  • asyncio.gather:
    • Menjalankan beberapa coroutine secara bersamaan.
    • Kekurangan: penanganan error kurang baik. Jika exception terjadi di satu task, task lain yang sedang berjalan akan dibatalkan.
  • asyncio.create_task:
    • Memungkinkan kontrol dan penanganan error per task.
    • Berguna untuk eksekusi di background, tetapi merepotkan karena beberapa task harus di-await satu per satu.
  • asyncio.TaskGroup (Python 3.11+):
    • Alternatif modern untuk 'structured concurrency'.
    • Mengelola grup task dengan sintaks async with, dan saat keluar dari konteks dipastikan semua task selesai atau exception telah ditangani.
    Iklan
    async with asyncio.TaskGroup() as tg:  
        tg.create_task(some_coro_1())  
        tg.create_task(some_coro_2())  
    # Saat blok 'async with' berakhir, semua task telah di-await  
    

Kesalahan 2: Menggunakan library sinkron

Jika library sinkron (blocking) seperti requests atau pathlib digunakan di dalam kode asyncio, seluruh event loop akan terblokir. Bahkan jika dipakai di dalam asyncio.gather, perilakunya tetap pada praktiknya menjadi berurutan.

  • Solusi: Gunakan library khusus yang mendukung asinkron (non-blocking) seperti aiohttp (pengganti requests), aiofiles (pengganti files/pathlib), dan sejenisnya.

Kesalahan 3: Event loop terblokir oleh pekerjaan CPU-bound

Karena asyncio berjalan di satu thread, pekerjaan komputasi berat (CPU-bound) akan menghentikan event loop dan menunda pekerjaan I/O lainnya.

  • Solusi: Gunakan loop.run_in_executor() untuk memindahkan pekerjaan CPU-bound ke thread pool terpisah (default) atau process pool.
    loop = asyncio.get_running_loop()  
    # Menjalankan fungsi yang intensif CPU di thread terpisah  
    await loop.run_in_executor(  
        None,  # gunakan thread pool default  
        cpu_bound_function,  
        arg1  
    )  
    
Iklan

Kesalahan 4: Pemblokiran akibat pekerjaan yang tidak penting

Jika pekerjaan non-inti seperti logging yang tidak terkait dengan respons pengguna tetap di-await, waktu respons akan tertunda secara tidak perlu.

  • Solusi: Gunakan asyncio.create_task() untuk memisahkan pekerjaan tersebut sebagai background task dan jangan di-await.
    user_profile = await get_user_profile()  
    # Jalankan logging di background tanpa await  
    asyncio.create_task(send_logs_to_external_service())  
    return user_profile  
    

Kesalahan 5: Membuat terlalu banyak task

Jika pekerjaan yang sangat kecil dijadikan task dalam jumlah besar, overhead context switching bisa muncul dan menurunkan performa.

  • Solusi 1: Gabungkan pekerjaan kecil (batching) menjadi beberapa task yang lebih besar.
  • Solusi 2: Gunakan asyncio.Semaphore untuk membatasi jumlah maksimum task yang berjalan bersamaan.
    # Izinkan maksimal 10 pekerjaan berjalan bersamaan  
    semaphore = asyncio.Semaphore(10)  
    
    async with semaphore:  
        await fetch_data()  
    
Iklan

Kesalahan lainnya

  • Coroutine "Never Awaited": Coroutine dipanggil tetapi tidak di-await, sehingga pekerjaannya bahkan tidak dijalankan dan gagal secara diam-diam. Ini dapat dideteksi dengan linter seperti flake8-async.
  • Manajemen resource yang tidak tepat: Jika file, koneksi DB, dan sejenisnya digunakan tanpa try...finally, kebocoran resource bisa terjadi. Solusinya adalah memakai asynchronous context manager dengan async with.

Debugging dan pemilihan model konkurensi

Mode debug Asyncio

Jika mode debug yang secara default nonaktif diaktifkan (asyncio.run(debug=True)), hal ini membantu mendeteksi masalah seperti berikut.

  • Coroutine yang tidak di-await (RuntimeWarning).
  • API asinkron yang dipanggil dari thread yang salah.
  • Callback dengan waktu eksekusi melebihi 100ms.
  • Operasi selector I/O yang lambat.

Alat debugging lainnya

  • Scalene: Profiler CPU dan memori.
  • aio-monitor: Monitoring dan CLI untuk aplikasi asyncio.
  • pdb: Debugger bawaan Python.
  • py-stack: Menampilkan stack trace proses Python yang sedang berjalan untuk mendeteksi titik pemblokiran.

Panduan memilih model konkurensi

  • Asyncio (single-thread): Paling cocok untuk banyak pekerjaan I/O-bound dengan latensi tinggi, misalnya request jaringan atau file I/O.
  • Threads (multi-thread): Digunakan untuk pekerjaan I/O-bound yang membutuhkan akses ke data bersama. Karena GIL (Global Interpreter Lock), ini bukan paralelisme sejati, tetapi thread lain bisa berjalan saat menunggu I/O.
  • Processes (multi-process): Digunakan untuk pekerjaan CPU-bound seperti pemrosesan gambar atau komputasi berat. Dapat memanfaatkan banyak inti CPU untuk mencapai paralelisme sejati, tetapi overhead memori dan komunikasi lebih besar.

https://youtu.be/wGDOwNW6lVk

12 komentar

 
savvykang 2025-11-18

Python memang bahasa yang hebat, tetapi antarmuka asinkronnya tampaknya merupakan fitur yang dirancang dengan buruk.

 
ceruns 2025-11-17

Nomor 4 melewatkan eager_start=True. Karena create_task membuat weakref, ini jadi kode yang menghasilkan task yang mungkin tidak akan pernah dijalankan....

 
tested 2025-11-17

https://rosettalens.com/s/ko/python-to-node

Orang ini juga katanya beralih ke Node.js gara-gara async Python

 
kandk 2025-11-17

Kesimpulan: antarmuka asinkron Python masih belum intuitif.

 
bungker 2025-11-17

Sebenarnya, kalau proyeknya sudah sampai perlu mengoptimalkan asynchronous Python, menulisnya dengan bahasa lain jauh lebih baik dari sisi performa maupun stabilitas.

 
euphcat 2025-11-17

Kalau tidak beralih ke bahasa yang dikompilasi, apakah perbedaan performanya akan besar? Kalau multithreading, tentu akan ada perbedaan besar karena adanya GIL, tetapi kalau strukturnya asinkron dengan event loop yang bekerja, saya penasaran perbedaan apa yang muncul tergantung bahasanya.

 
vwjdalsgkv 2025-11-17

Perbedaan ada/tidaknya kompilasi JIT ternyata lebih besar dari yang dibayangkan. V8 dioptimalkan dengan sangat baik.

 
euphcat 2025-11-16

Saya belum mengecek video sumbernya, tetapi kode solusi untuk kesalahan 4 itu salah.

Instance task yang dikembalikan oleh create_task() harus ditugaskan ke setidaknya satu variabel, dan variabel tersebut harus tetap hidup sampai task selesai. Jika tidak, ada risiko instance task terkena garbage collection saat coroutine masih berjalan.

Jika fungsi yang membuat task seperti di atas segera berakhir, sebaiknya gunakan cara seperti mengembalikan instance task, menugaskannya ke variabel global, atau menugaskannya ke variabel instance.

P.S.)
Meskipun nilai kembalian memang tidak diperlukan dan Anda yakin coroutine akan selesai dalam waktu singkat, untuk instance task tetap lebih baik ditulis agar pada suatu saat dilakukan await. Kalau tidak suka begitu, setidaknya pasang penanganan exception yang ketat pada setiap coroutine yang akan dijalankan sebagai task, lalu siapkan struktur logging yang menampilkan pesan log tanpa celah. Kalau tidak, bisa terjadi Exception tidak tertangani dan gagal secara diam-diam meskipun task menimbulkan masalah besar.

Di proyek yang saya kembangkan/kelola untuk mencari nafkah, saya pernah merancang pola di mana puluhan modul masing-masing membuat satu task seperti while self.ok(): cmd = await self.cmd_queue.get(); await self.process(cmd); lalu terus menjalankannya. Sebelum pola penanganan exception-nya mapan, setiap kali satu masalah meledak, mental saya juga ikut meledak—pengalaman langka, haha.

 
kunggom 2025-11-16

Bahkan dari sudut pandang saya yang bekerja di perusahaan yang memakai C#, yang bisa dibilang pelopor pola Async/Await, saya juga cukup sering melihat kode yang salah seperti kesalahan nomor 1, yaitu menyusun await secara sederhana berurutan satu per satu.

Saat melihat kode seperti itu, entah bagaimana, saya merasa ada pola yang sama: mereka hanya tahu bahwa di depan pemanggilan method async harus memakai kata kunci await, tetapi tidak terlalu memikirkan lebih jauh soal urutan eksekusi asinkron, sehingga kode seperti ini pun muncul.
Ketika ada beberapa await, untuk sebagian hasilnya dipakai tepat di bawahnya sehingga di situ langsung menerima nilai hasil await dari objek Task<T>, sedangkan untuk sebagian lain hasilnya baru dipakai cukup jauh di bawah sehingga cukup menerima Task<T> dulu lalu baru di-await nanti; menulis kode dengan mempertimbangkan alur asinkron seperti ini memang pekerjaan yang menuntut pemikiran lebih.

Setidaknya saya sendiri menulis kode di method yang dideklarasikan asinkron dengan mempertimbangkan alur pemrosesan seperti itu. Namun, kadang saat melihat kode peninggalan mantan karyawan yang sedang saya maintenance, ada kalanya saya mendapat kesan, ‘Saya sebenarnya cuma ingin menulis kode sinkron secara sederhana, tapi karena method yang harus dipakai di tengah hanya tersedia dalam tipe asinkron, ya sudah saya tulis begini saja.’

 
skageektp 2025-11-17

Kalau nomor 1 selalu independen, memang bagus dilakukan seperti itu,
tapi kalau setelah kode diubah jadi tidak independen, rasanya merepotkan juga karena harus meninjau dan memperbaiki semua tempat yang memakai fungsi itu.
Kalau pekerjaannya tidak memakan waktu terlalu lama, mungkin melakukan await secara serial lebih baik dari sisi pengelolaan kode

 
euphcat 2025-11-17

Sepertinya konsep ini perlu didekati sebagai, “karena overhead multithreading itu memberatkan, alternatif berikutnya adalah memecah single thread untuk menangani pemrosesan paralel.” Karena itu, pada dasarnya memang benar bahwa dibanding multithreading, dalam kondisi tertentu pendekatan ini justru perlu mendapat perhatian yang lebih besar.

 
kunggom 2025-11-17

Memang begitu.
Sepertinya kode asinkron yang benar-benar baik pada dasarnya adalah jenis kode yang memang menuntut banyak perhatian.