Back to IF4031 Arsitektur Aplikasi Terdistribusi

Concurrent Programming dengan Python Asyncio

Questions/Cues

  • Masalah Threading?

  • Konsumsi Memori?

  • Apa itu Asyncio?

  • Sintaks async/await?

  • Struktur dasar asyncio?

  • Cara menjalankan program?

  • Apa itu Coroutine?

  • Bagaimana Coroutine dieksekusi?

  • Menangani Blocking Code?

  • Peran Executor?

  • run_in_executor?

Reference Points

  • IF4031-04-2022-Async-Library.pdf

Masalah pada Model Threading Tradisional

Meskipun threading adalah cara yang umum untuk mencapai konkurensi (menjalankan banyak tugas seolah-olah bersamaan), model ini memiliki kelemahan signifikan, terutama dalam hal penggunaan sumber daya.

Contoh Konsumsi Memori

Seperti yang ditunjukkan pada slide, membuat sejumlah besar thread (misalnya 10.000) dapat mengonsumsi memori dalam jumlah yang sangat besar (contoh menunjukkan 82 GB). Hal ini terjadi karena setiap thread memerlukan alokasi stack memori sendiri dari sistem operasi, terlepas dari apakah thread tersebut aktif bekerja atau hanya menunggu (misalnya, sleep). Untuk aplikasi yang perlu menangani ribuan koneksi jaringan secara bersamaan (C10k problem), pendekatan satu-thread-per-koneksi menjadi tidak praktis karena boros memori.

Pengenalan Python Asyncio

Asyncio adalah library di Python yang menyediakan infrastruktur untuk menulis kode konkuren single-threaded menggunakan sintaks async/await. Ini adalah implementasi tingkat tinggi dari konsep event loop yang telah kita bahas sebelumnya.

Prinsip Utama asyncio:

  1. Menjalankan Event Loop: asyncio mengelola event loop yang memantau tugas-tugas dan menjalankan callback ketika event (seperti selesainya operasi I/O) terjadi.

  2. Eksekusi Berdasarkan Event: Tugas-tugas (coroutines) dieksekusi oleh event loop.

  3. Yield Voluntarily: Sebuah tugas asyncio akan berjalan sampai ia secara eksplisit “memberikan kembali” kontrol ke event loop. Ini biasanya terjadi saat ia menemui kata kunci await pada operasi yang butuh waktu (misalnya, asyncio.sleep() atau operasi jaringan). Saat menunggu, event loop bebas untuk menjalankan tugas lain yang sudah siap.

Sintaks async dan await

  • async def: Digunakan untuk mendeklarasikan sebuah fungsi sebagai coroutine. Ini adalah unit dasar yang dapat dijalankan oleh asyncio.

  • await: Digunakan di dalam sebuah coroutine untuk memanggil coroutine lain. Saat await dipanggil, fungsi yang sedang berjalan akan “berhenti sejenak” (pause), memberikan kontrol kembali ke event loop untuk menjalankan tugas lain. Ketika coroutine yang di-await selesai, eksekusi akan dilanjutkan dari titik di mana ia berhenti.

Struktur Dasar dan Eksekusi

Cara termudah untuk menjalankan sebuah program asyncio (di Python 3.7+) adalah dengan menggunakan asyncio.run().

import asyncio
 
async def main():
 
	print('Hello ...')
	
	# Berhenti sejenak di sini selama 1 detik,
	# event loop bisa menjalankan tugas lain jika ada.
	await asyncio.sleep(1)
	print('... World!')
	
	# asyncio.run() akan membuat event loop baru,
	# menjalankan coroutine main() hingga selesai
	# lalu menutup loop secara otomatis.
 
	asyncio.run(main())

Coroutines

Secara formal, coroutine adalah sebuah objek yang merangkum kemampuan untuk melanjutkan sebuah fungsi yang telah ditangguhkan (suspended) sebelum selesai. Sederhananya, ini adalah sebuah fungsi yang eksekusinya bisa dijeda dan dilanjutkan. Di Python, fungsi yang didefinisikan dengan async def akan mengembalikan objek coroutine saat dipanggil.

Cara mengeksekusi sebuah coroutine:

  1. await coro: Cara paling umum, digunakan di dalam coroutine lain.

  2. loop.create_task(coro): Menjadwalkan coroutine untuk segera dijalankan di event loop tanpa harus menunggunya selesai. Ini akan membungkus coroutine dalam sebuah objek Task.

  3. asyncio.run(coro): Titik masuk utama untuk memulai eksekusi program asyncio.

Menangani Kode Blocking dengan Executor

Salah satu tantangan terbesar dalam pemrograman asinkron adalah bagaimana menangani kode yang bersifat blocking (misalnya, fungsi dari library pihak ketiga yang tidak mendukung asyncio, atau operasi CPU-bound yang intensif). Jika kita memanggil fungsi blocking (seperti time.sleep()) langsung di dalam coroutine, seluruh event loop akan terhenti dan membeku.

Peran Executor

Solusinya adalah dengan menggunakan Executor. asyncio dapat berintegrasi dengan executor (seperti ThreadPoolExecutor) untuk menjalankan kode blocking di thread terpisah.

loop.run_in_executor(executor, func, *args)

Fungsi ini menjadwalkan func untuk dijalankan di executor yang ditentukan.

  • executor: Bisa berupa ThreadPoolExecutor atau ProcessPoolExecutor. Jika None, ThreadPoolExecutor default akan digunakan.

  • func: Fungsi blocking yang ingin dijalankan.

  • *args: Argumen untuk fungsi tersebut.

run_in_executor akan mengembalikan objek Future yang bisa di-await. Ini memungkinkan kode asinkron kita untuk menunggu hasil dari kode blocking tanpa membekukan event loop.

import time
import asyncio
 
 
def blocking_io():
	print("Start blocking operation")
 
	# Simulasi operasi I/O yang blocking
	time.sleep(2)
	
	print("Blocking operation finished")
	return "Blocking result"
 
async def main():
	loop = asyncio.get_running_loop()
	print("Running blocking code in executor")
 
 
	# Jalankan fungsi blocking di thread pool default
	result = await loop.run_in_executor(
	    None, blocking_io
	)
	print(f"Result from blocking code: {result}")
 
asyncio.run(main())

Saat await loop.run_in_executor() dieksekusi, event loop akan mendelegasikan blocking_io() ke thread lain dan bebas menjalankan tugas lain. Ketika blocking_io() selesai, event loop akan melanjutkan eksekusi main() setelah await.

Summary

Python asyncio memungkinkan konkurensi yang efisien secara memori pada satu thread dengan menggunakan event loop dan sintaks async/await untuk mengelola coroutines. Coroutines adalah fungsi yang dapat dijeda (await) untuk memberikan kontrol kembali ke event loop, yang kemudian dapat menjalankan tugas lain. Untuk mengintegrasikan kode blocking tanpa menghentikan event loop, asyncio menggunakan Executor untuk menjalankan operasi tersebut di thread terpisah, memungkinkan kode asinkron untuk menunggu hasilnya secara non-blocking.