Percepat kode Anda dengan pemrograman async di Python. Panduan langkah demi langkah untuk asyncio, konkurensi, permintaan HTTP efisien, dan integrasi database.
Saat skrip Python Anda dengan sabar menunggu respons API, kueri database, atau operasi berkas selesai, waktu itu sering kali tidak terpakai. Dengan pemrograman async Python, kode Anda dapat menangani beberapa tugas sekaligus. Jadi ketika satu operasi menunggu, yang lain tetap berjalan, mengubah momen menganggur menjadi pekerjaan produktif dan sering kali memangkas waktu tunggu dari menit menjadi hitungan detik.
Dalam panduan ini, saya akan mengajarkan dasar-dasar pemrograman async di Python melalui mini-proyek. Anda akan melihat bagaimana coroutine, event loop, dan async I/O dapat membuat kode Anda jauh lebih responsif.
Jika Anda ingin mempelajari cara membangun web API asinkron, pastikan untuk melihat kursus tentang FastAPI.
Apa itu Pemrograman Async Python?
Dalam Python sinkron tradisional, kode Anda dieksekusi satu baris pada satu waktu. Misalnya, saat Anda memanggil sebuah API, program Anda berhenti dan menunggu respons. Jika itu memakan waktu dua detik, seluruh program Anda menganggur selama dua detik. Pemrograman asinkron memungkinkan kode Anda memulai panggilan API lalu melanjutkan tugas lain.
Saat respons tiba, kode Anda melanjutkan dari tempat semula. Alih-alih menunggu tiap operasi selesai, Anda dapat menjalankan beberapa operasi secara bersamaan. Hal ini paling penting ketika kode Anda menghabiskan waktu menunggu sistem eksternal seperti database, API, atau sistem berkas merespons.
Agar ini berhasil, sistem async Python menggunakan beberapa konsep inti:
Coroutine: Fungsi yang didefinisikan dengan async def alih-alih def. Fungsi ini dapat jeda dan dilanjutkan kembali, sehingga ideal untuk operasi yang melibatkan penantian.
await: Kata kunci yang memberi tahu Python, "jeda coroutine ini sampai operasi ini selesai, tetapi biarkan kode lain berjalan sementara itu."
Event loop: Mesin yang mengelola semua coroutine Anda, memutuskan mana yang dijalankan dan kapan beralih di antaranya.
Task: Coroutine yang dibungkus untuk eksekusi bersamaan. Anda membuatnya dengan asyncio.create_task() untuk menjalankan beberapa operasi sekaligus.
Agar tidak bingung tentang apa yang bisa (dan tidak bisa) dilakukan async programming, ingat hal ini:
Async paling efektif untuk pekerjaan I/O-bound seperti permintaan HTTP, kueri database, dan operasi berkas, saat kode Anda menunggu sistem eksternal.
Async tidak membantu untuk pekerjaan CPU-bound seperti perhitungan kompleks atau pemrosesan data, saat kode Anda aktif menghitung alih-alih menunggu.
Cara terbaik untuk memahaminya adalah dengan menulis kode async sebenarnya. Pada bagian berikutnya, Anda akan membuat fungsi async pertama Anda dan melihat persis bagaimana coroutine dan event loop bekerja bersama.
Fungsi Async Python Pertama Anda
Sebelum menulis kode async, mari lihat fungsi sinkron biasa yang menunggu sebelum melakukan sesuatu:
import time
defgreet_after_delay():print("Starting...")
time.sleep(2)# Blocks for 2 secondsprint("Hello!")
greet_after_delay()Didukung oleh
Fungsinya bekerja, tetapi time.sleep(2) memblokir seluruh program Anda. Tidak ada yang bisa berjalan selama dua detik itu.
Sekarang berikut versi async-nya:
import asyncio
asyncdefgreet_after_delay():print("Starting...")await asyncio.sleep(2)# Pauses, but doesn't blockprint("Hello!")
asyncio.run(greet_after_delay())Didukung oleh
Keluaran terlihat identik, tetapi ada hal berbeda yang terjadi di balik layar. Tiga perubahan membuatnya async:
async def alih-alih def mendeklarasikan ini sebagai coroutine.
await asyncio.sleep(2) alih-alih time.sleep(2) menjeda tanpa memblokir.
asyncio.run() memulai event loop dan menjalankan coroutine.
Perhatikan bahwa asyncio.sleep() sendiri adalah fungsi async, itulah sebabnya perlu await. Ini aturan kunci: setiap fungsi async harus dipanggil dengan await. Baik itu bawaan seperti asyncio.sleep() atau buatan Anda sendiri, lupa await berarti fungsi itu tidak benar-benar dieksekusi.
Saat ini, versi async belum terlihat lebih cepat. Itu karena kita hanya punya satu tugas. Manfaat sebenarnya muncul saat Anda menjalankan beberapa coroutine sekaligus, yang akan kita bahas di bagian berikut.
Satu hal penting lainnya: Anda tidak bisa memanggil fungsi async langsung seperti fungsi biasa. Mari kita coba:
result = greet_after_delay()print(result)print(type(result))Didukung oleh
<coroutine object greet_after_delay at 0x...>
<class 'coroutine'>Didukung oleh
Memanggil greet_after_delay() mengembalikan objek coroutine, bukan hasilnya. Fungsinya tidak benar-benar berjalan. Anda memerlukan asyncio.run() atau await untuk mengeksekusinya di dalam fungsi lain.
Cara kerja event loop
Event loop adalah mesin di balik pemrograman async. Ia mengelola coroutine Anda dan memutuskan apa yang dijalankan kapan. Berikut yang terjadi langkah demi langkah saat Anda menjalankan fungsi async greet_after_delay():
asyncio.run() membuat event loop.
Event loop memulai greet_after_delay().
"Starting..." tercetak.
Mencapai await asyncio.sleep(2) → coroutine dijeda.
Event loop memeriksa: "Ada tugas lain untuk dijalankan?" (belum ada).
2 detik berlalu, sleep selesai.
Event loop melanjutkan greet_after_delay().
"Hello!" tercetak.
Fungsi selesai → event loop keluar.
Langkah 5 adalah tempat async menjadi menarik. Dengan satu coroutine, tidak ada yang lain untuk dilakukan. Namun saat Anda memiliki banyak coroutine, event loop beralih ke pekerjaan lain sementara yang satu menunggu. Alih-alih menganggur selama tidur dua detik, ia dapat menjalankan kode lain.
Anggap event loop seperti pengatur lalu lintas. Ia tidak membuat mobil individual lebih cepat. Ia menjaga lalu lintas tetap bergerak dengan membiarkan mobil lain jalan saat satu mobil berhenti.
Kesalahan async yang umum: Lupa await
Kesalahan pemula yang umum adalah lupa await saat memanggil coroutine di dalam fungsi async lain:
Saat Anda melihat RuntimeWarning tentang coroutine yang tidak di-await, periksa bahwa Anda sudah menggunakan await pada setiap pemanggilan fungsi async.
Tugas Async Python yang Berjalan Bersamaan
Pada bagian sebelumnya, kita mengonversi fungsi sinkron menjadi async. Namun itu tidak lebih cepat. Itu karena kita hanya menjalankan satu coroutine. Kekuatan nyata async muncul saat Anda menjalankan beberapa coroutine pada saat yang sama.
Mengapa await berurutan tetap berurutan
Anda mungkin berpikir memanggil beberapa fungsi async akan otomatis menjalankannya secara bersamaan. Namun lihat apa yang terjadi saat kita memanggil greet_after_delay() tiga kali:
Starting Alice...
Hello, Alice!
Starting Bob...
Hello, Bob!
Starting Charlie...
Hello, Charlie!
Total time: 6.01 secondsDidukung oleh
Enam detik untuk tiga tugas berdurasi dua detik. Setiap await menunggu coroutine-nya selesai sebelum pindah ke baris berikutnya. Kodenya async, tetapi berjalan berurutan.
Menjalankan tugas async secara bersamaan dengan asyncio.gather()
Untuk menjalankan coroutine pada saat yang sama, gunakan asyncio.gather(). Fungsi ini menerima beberapa coroutine dan menjalankannya secara bersamaan:
Starting Alice...
Starting Bob...
Starting Charlie...
Hello, Alice!
Hello, Bob!
Hello, Charlie!
Total time: 2.00 secondsDidukung oleh
Dua detik alih-alih enam. Ketiga coroutine dimulai segera, tidur secara bersamaan, dan selesai bersama. Itu peningkatan 3x hanya dengan satu perubahan.
Perhatikan urutan keluaran: ketiga pesan "Starting..." tercetak sebelum pesan "Hello..." mana pun. Ini menunjukkan semua coroutine berjalan dalam jendela dua detik yang sama alih-alih saling menunggu.
asyncio.gather() mengembalikan daftar hasil dalam urutan yang sama dengan urutan Anda meneruskan coroutine. Jika coroutine Anda mengembalikan nilai, Anda dapat menampungnya:
asyncdeffetch_number(n):await asyncio.sleep(1)return n *10asyncdefmain():
results =await asyncio.gather(
fetch_number(1),
fetch_number(2),
fetch_number(3),)print(results)
asyncio.run(main())Didukung oleh
Hasil kembali dalam urutan [10, 20, 30], sesuai urutan coroutine yang diteruskan ke gather().
Permintaan HTTP Async Python dengan aiohttp
Sejauh ini, kita menggunakan asyncio.sleep() untuk mensimulasikan jeda. Sekarang mari buat permintaan HTTP nyata. Anda mungkin ingin menggunakan pustaka requests, tetapi itu tidak cocok di sini. requests bersifat sinkron dan memblokir event loop, menggagalkan tujuan async.
Sebagai gantinya, gunakan aiohttp, klien HTTP async yang dibuat untuk tujuan ini.
Pengenalan aiohttp
Berikut cara mengambil sebuah URL dengan aiohttp:
import aiohttp
import asyncio
asyncdeffetch(url):asyncwith aiohttp.ClientSession()as session:asyncwith session.get(url)as response:returnawait response.text()asyncdefmain():
html =await fetch("https://example.com")print(f"Fetched {len(html)} characters")
asyncio.run(main())Didukung oleh
Perhatikan dua async with bertingkat. Masing-masing mengelola sumber daya berbeda, dan memahami perannya adalah kunci menggunakan aiohttp dengan benar.
Cara kerja ClientSession di aiohttp
Berikut yang terjadi langkah demi langkah saat Anda membuat permintaan dengan aiohttp:
aiohttp.ClientSession() membuat connection pool (awal kosong).
session.get(url) memeriksa pool: "Ada koneksi terbuka ke host ini (server situs)?"
Jika belum ada, dibuat koneksi TCP baru (protokol dasar untuk mengirim data melalui internet) dan jabat tangan SSL (penyiapan enkripsi untuk HTTPS).
Permintaan HTTP dikirim, dan kita menunggu header respons.
Objek respons memegang koneksi.
await response.text() membaca data body dari jaringan.
Keluar dari async with bagian dalam: Koneksi kembali ke pool (tetap terbuka!).
Permintaan berikutnya ke host yang sama dibuat, menggunakan ulang koneksi dari pool (melewati langkah 3).
Keluar dari async with luar: Semua koneksi dalam pool ditutup.
Langkah 7 dan 8 adalah wawasan kunci. Connection pool menjaga koneksi tetap hidup di antara permintaan. Saat Anda membuat permintaan lain ke host yang sama, ia melewati proses TCP dan jabat tangan SSL sepenuhnya.
Ini penting karena membangun koneksi baru itu lambat. Jabat tangan TCP membutuhkan satu perjalanan bolak-balik ke server. Jabat tangan SSL membutuhkan dua lagi. Bergantung pada latensi, itu 100–300 ms sebelum Anda bahkan mengirim byte data pertama.
Menggunakan sesi bersama untuk semua permintaan
Sekarang Anda bisa melihat mengapa membuat sesi baru untuk setiap permintaan adalah masalah:
# Wrong: new session for each requestasyncdeffetch_bad(url):asyncwith aiohttp.ClientSession()as session:asyncwith session.get(url)as response:returnawait response.text()asyncdefmain():
urls =["https://example.com"]*10
results =await asyncio.gather(*[fetch_bad(url)for url in urls])Didukung oleh
Setiap pemanggilan fetch_bad() membuat sesi baru dengan pool kosong. Setiap permintaan membayar biaya jabat tangan penuh, meskipun semuanya ke host yang sama.
Solusinya adalah membuat satu sesi dan meneruskannya ke fungsi fetch Anda:
# Right: reuse a single sessionasyncdeffetch_good(session, url):asyncwith session.get(url)as response:returnawait response.text()asyncdefmain():
urls =["https://example.com"]*10asyncwith aiohttp.ClientSession()as session:
results =await asyncio.gather(*[fetch_good(session, url)for url in urls])Didukung oleh
Dengan sesi bersama, permintaan pertama membangun koneksi, dan sembilan permintaan berikutnya menggunakannya kembali. Satu jabat tangan alih-alih sepuluh.
Contoh permintaan HTTP async: Mengambil data Hacker News
Mari praktikkan ini dengan API Hacker News. API ini sempurna untuk mendemonstrasikan perilaku asinkron karena mengambil cerita memerlukan beberapa permintaan. Jika Anda baru bekerja dengan REST API di Python, lihat Python APIs: A Guide to Building and Using APIs untuk konsep dasar.
Struktur API Hacker News:
https://hacker-news.firebaseio.com/v0/topstories.json mengembalikan daftar ID cerita (hanya angka)
https://hacker-news.firebaseio.com/v0/item/{id}.json mengembalikan detail untuk satu cerita
Untuk mendapatkan 10 cerita, Anda memerlukan 11 permintaan: satu untuk daftar ID, lalu satu untuk tiap cerita. Di sinilah pemrograman async bersinar.
Pertama, mari lihat apa yang dikembalikan API jika kita mencoba mengambil cerita pertama:
import aiohttp
import asyncio
HN_API ="https://hacker-news.firebaseio.com/v0"asyncdefmain():asyncwith aiohttp.ClientSession()as session:# Get top story IDsasyncwith session.get(f"{HN_API}/topstories.json")as response:
story_ids =await response.json()print(f"Found {len(story_ids)} stories")print(f"First 5 IDs: {story_ids[:5]}")# Fetch first story details
first_id = story_ids[0]asyncwith session.get(f"{HN_API}/item/{first_id}.json")as response:
story =await response.json()print(f"\nStory structure:")for key, value in story.items():print(f" {key}: {repr(value)[:50]}")
asyncio.run(main())Didukung oleh
Found 500 stories
First 5 IDs: [46051449, 46055298, 46021577, 46053566, 45984864]
Story structure:
by: 'mikeayles'
descendants: 22
id: 46051449
kids: [46054027, 46053889, 46053275, 46053515, 46053002,
score: 217
text: 'I got DOOM running in KiCad by rendering it with
time: 1764108815
title: 'Show HN: KiDoom – Running DOOM on PCB Traces'
type: 'story'
url: 'https://www.mikeayles.com/#kidoom'Didukung oleh
API mengembalikan 500 ID cerita, dan setiap cerita memiliki field seperti title, url, score, dan by (penulis).
Mengambil banyak hasil secara berurutan vs. bersamaan
Sekarang mari ambil 10 cerita secara berurutan:
import aiohttp
import asyncio
import time
HN_API ="https://hacker-news.firebaseio.com/v0"asyncdeffetch_story(session, story_id):asyncwith session.get(f"{HN_API}/item/{story_id}.json")as response:returnawait response.json()asyncdefmain():asyncwith aiohttp.ClientSession()as session:asyncwith session.get(f"{HN_API}/topstories.json")as response:
story_ids =await response.json()
start = time.perf_counter()
stories =[]for story_id in story_ids[:10]:
story =await fetch_story(session, story_id)
stories.append(story)
elapsed = time.perf_counter()- start
print(f"Sequential: Fetched {len(stories)} stories in {elapsed:.2f} seconds")
asyncio.run(main())Didukung oleh
Sequential: Fetched 10 stories in 2.41 secondsDidukung oleh
Sekarang mari ambil cerita yang sama secara bersamaan:
asyncdefmain():asyncwith aiohttp.ClientSession()as session:asyncwith session.get(f"{HN_API}/topstories.json")as response:
story_ids =await response.json()
start = time.perf_counter()
tasks =[fetch_story(session, story_id)for story_id in story_ids[:10]]
stories =await asyncio.gather(*tasks)
elapsed = time.perf_counter()- start
print(f"Concurrent: Fetched {len(stories)} stories in {elapsed:.2f} seconds")print("\nTop 3 stories:")for story in stories[:3]:print(f" - {story.get('title','No title')}")
asyncio.run(main())Didukung oleh
Concurrent: Fetched 10 stories in 0.69 seconds
Top 3 stories:
- Show HN: KiDoom – Running DOOM on PCB Traces
- AWS is 10x slower than a dedicated server for the same price [video]
- Surprisingly, Emacs on Android is pretty goodDidukung oleh
Versi bersamaan 3,5x lebih cepat. Alih-alih menunggu tiap permintaan selesai sebelum memulai berikutnya, semua 10 permintaan berjalan pada saat yang sama. Di sinilah pemrograman async memberikan hasil nyata pada I/O jaringan.
Penanganan Error dan Pembatasan Laju pada Async Python
Saat mengambil data secara bersamaan, beberapa hal bisa salah. Anda bisa membebani server dengan terlalu banyak permintaan. Beberapa permintaan bisa menggantung selamanya. Yang lain mungkin langsung gagal. Dan ketika kegagalan terjadi, Anda memerlukan strategi pemulihan.
Bagian ini membahas tiap kekhawatiran sesuai urutan terjadinya: mengontrol berapa banyak permintaan yang keluar, menetapkan batas waktu, menangani kegagalan, dan mencoba ulang saat masuk akal. Jika Anda perlu penyegaran tentang dasar-dasar penanganan pengecualian Python, lihat Exception & Error Handling in Python. Kita akan menggunakan setelan dasar ini sepanjang pembahasan:
import aiohttp
import asyncio
import time
HN_API ="https://hacker-news.firebaseio.com/v0"asyncdeffetch_story(session, story_id):asyncwith session.get(f"{HN_API}/item/{story_id}.json")as response:returnawait response.json()Didukung oleh
Pembatasan laju dengan semaphore
Pada bagian sebelumnya, kita menembakkan 10 permintaan sekaligus. Itu berjalan baik. Namun bagaimana saat Anda perlu mengambil 500 cerita? Atau meng-crawl 10.000 halaman?
Sebagian besar API menerapkan rate limit. Mereka mungkin mengizinkan 10 permintaan per detik, atau 100 koneksi bersamaan. Jika melampaui batas, Anda akan diblokir, diperlambat, atau dibanned. Bahkan jika API tidak menerapkan batas, menembakkan ribuan permintaan sekaligus bisa membebani sistem Anda sendiri atau server.
Anda butuh cara untuk mengontrol berapa banyak permintaan yang “sedang berlangsung” pada momen tertentu. Itulah peran semaphore.
Semaphore bekerja seperti sistem izin. Bayangkan Anda memiliki tiga izin. Tugas mana pun yang ingin membuat permintaan harus terlebih dahulu mendapatkan izin. Saat selesai, ia mengembalikan izin, sehingga permintaan baru bisa menggunakannya. Jika tidak ada izin yang tersedia, tugas menunggu sampai ada yang bebas.
Begini jalannya dengan 3 izin dan 4 atau lebih tugas:
Tersedia tiga izin.
Tugas A mengambil satu izin (tersisa 2), memulai permintaan.
Tugas B mengambil satu izin (tersisa 1), memulai permintaan.
Tugas C mengambil satu izin (tersisa 0), memulai permintaan.
Tugas D ingin izin, tetapi tidak ada yang tersedia—ia menunggu.
Tugas A selesai, mengembalikan izinnya (tersedia 1).
Tugas D mengambil izin tersebut dan memulai permintaan.
Ini berlanjut hingga semua tugas selesai.
Penungguan pada langkah 5 efisien. Tugas tidak berputar dalam loop memeriksa "apakah ada izin yang bebas?". Ia disuspensi dan membiarkan kode lain berjalan. Event loop membangunkannya hanya saat izin tersedia.
Sekarang lihat kodenya. Dalam asyncio, Anda membuat semaphore dengan asyncio.Semaphore(n), di mana n adalah jumlah izin. Untuk menggunakannya, bungkus kode Anda dalam async with semaphore:. Ini memperoleh izin saat memasuki blok dan otomatis melepaskannya saat keluar:
asyncdeffetch_story_limited(session, story_id, semaphore):asyncwith semaphore:# Acquire permit (or wait if none available)asyncwith session.get(f"{HN_API}/item/{story_id}.json")as response:returnawait response.json()# Permit automatically released hereDidukung oleh
Mari bandingkan mengambil 30 cerita dengan dan tanpa semaphore:
asyncdefmain():asyncwith aiohttp.ClientSession()as session:asyncwith session.get(f"{HN_API}/topstories.json")as response:
story_ids =(await response.json())[:30]# Without rate limiting: all 30 at once
start = time.perf_counter()await asyncio.gather(*[fetch_story(session, sid)for sid in story_ids])print(f"No limit: {time.perf_counter()- start:.2f}s (30 concurrent)")# With Semaphore(5): max 5 at a time
semaphore = asyncio.Semaphore(5)
start = time.perf_counter()await asyncio.gather(*[fetch_story_limited(session, sid, semaphore)for sid in story_ids])print(f"Semaphore(5): {time.perf_counter()- start:.2f}s (5 concurrent)")
asyncio.run(main())Didukung oleh
No limit: 0.62s (30 concurrent)
Semaphore(5): 1.50s (5 concurrent)Didukung oleh
Versi semaphore lebih lambat karena memproses permintaan dalam batch berisi lima. Namun itulah konsekuensinya: Anda mengorbankan kecepatan demi perilaku yang lebih dapat diprediksi dan ramah server.
Satu hal yang perlu dicatat: semaphore membatasi permintaan yang bersamaan, bukan permintaan per satuan waktu. Semaphore(10) berarti "maksimal 10 permintaan sedang berlangsung sekaligus," bukan "10 permintaan per detik." Jika Anda membutuhkan pembatasan berbasis waktu yang ketat (seperti tepat 10 permintaan per detik), Anda bisa menggabungkan semaphore dengan jeda antar batch, atau gunakan pustaka seperti aiolimiter.
Timeout dengan asyncio.wait_for()
Bahkan dengan konkurensi terkontrol, permintaan individual bisa menggantung. Server mungkin menerima koneksi Anda tetapi tidak pernah merespons. Tanpa timeout, program Anda menunggu tanpa batas.
Fungsi asyncio.wait_for() membungkus coroutine apa pun dengan tenggat waktu. Anda memberinya coroutine dan timeout dalam detik. Jika operasi tidak selesai tepat waktu, fungsi ini melempar asyncio.TimeoutError:
asyncdefslow_operation():print("Starting slow operation...")await asyncio.sleep(5)return"Done"asyncdefmain():try:
result =await asyncio.wait_for(slow_operation(), timeout=2.0)print(f"Success: {result}")except asyncio.TimeoutError:print("Operation timed out after 2 seconds")
asyncio.run(main())Didukung oleh
Starting slow operation...
Operation timed out after 2 secondsDidukung oleh
Saat timeout habis, wait_for() membatalkan coroutine. Anda bisa menangkap TimeoutError dan memutuskan apa yang harus dilakukan: lewati permintaan, kembalikan nilai bawaan, atau coba lagi.
Untuk permintaan bersamaan, bungkus masing-masing secara individual. Berikut helper yang mengembalikan kamus error alih-alih melempar error:
Saat coroutine dibatalkan (oleh timeout atau alasan lain), Python melempar asyncio.CancelledError di dalamnya. Jika coroutine Anda memegang sumber daya seperti file handle atau koneksi, gunakan try/finally untuk memastikan pembersihan tetap terjadi bahkan saat pembatalan:
asyncdeffetch_with_cleanup(session, url):print("Starting fetch...")try:asyncwith session.get(url)as response:returnawait response.text()finally:print("Cleanup complete")# Runs even on cancellationDidukung oleh
Penanganan error dengan asyncio.gather()
Timeout menangkap permintaan yang lambat. Namun beberapa permintaan gagal seketika dengan error. Mari lihat apa yang terjadi saat satu permintaan dalam satu batch gagal.
Pertama, kita butuh versi fetch_story() yang melempar pengecualian pada ID tidak valid:
asyncdeffetch_story_strict(session, story_id):
story =await fetch_story(session, story_id)if story isNone:raise ValueError(f"Story not found: {story_id}")return storyDidukung oleh
Sekarang mari ambil empat cerita valid plus satu ID tidak valid:
Dengan satu ID tidak valid, kita kehilangan keempat hasil yang berhasil. Secara bawaan, gather() menggunakan perilaku fail-fast: satu pengecualian membatalkan semuanya dan meneruskan ke atas.
Untuk mempertahankan hasil parsial, tambahkan return_exceptions=True. Ini mengubah perilaku gather(): alih-alih melempar pengecualian, ia mengembalikannya sebagai item dalam daftar hasil bersama nilai yang berhasil:
asyncdefmain():asyncwith aiohttp.ClientSession()as session:asyncwith session.get(f"{HN_API}/topstories.json")as response:
story_ids =await response.json()
ids_to_fetch = story_ids[:4]+[99999999999]
results =await asyncio.gather(*[fetch_story_strict(session, sid)for sid in ids_to_fetch],
return_exceptions=True# Don't raise, return exceptions in list)# Separate successes from failures using isinstance()
stories =[r for r in results ifnotisinstance(r, Exception)]
errors =[r for r in results ifisinstance(r, Exception)]print(f"Got {len(stories)} stories, {len(errors)} failed")
asyncio.run(main())Didukung oleh
Pemeriksaan isinstance(result, Exception) memungkinkan Anda memisahkan hasil yang berhasil dari error. Anda kemudian dapat memproses yang berhasil dan mencatat atau mencoba lagi yang gagal.
Logika retry dengan exponential backoff
Beberapa kegagalan bersifat sementara. Server mungkin kelebihan beban sesaat, atau gangguan jaringan menjatuhkan koneksi Anda. Untuk kasus ini, mencoba ulang masuk akal.
Namun mencoba ulang segera dapat memperburuk keadaan. Jika server sedang kesulitan, membombardirnya dengan retry akan menambah masalah. Exponential backoff menyelesaikan ini dengan menunggu lebih lama di antara tiap percobaan.
Pola ini menggunakan 2 ** attempt untuk menghitung waktu tunggu: percobaan 0 menunggu satu detik (2⁰), percobaan 1 menunggu dua detik (2¹), percobaan 2 menunggu empat detik (2²), dan seterusnya. Ini memberi server waktu pemulihan yang makin panjang:
asyncdeffetch_with_retry(session, story_id, max_retries=3):for attempt inrange(max_retries):try:
story =await fetch_story(session, story_id)if story isNone:raise ValueError(f"Story {story_id} not found")return story
except(aiohttp.ClientError, ValueError):# Catch specific exceptionsif attempt == max_retries -1:print(f"Story {story_id}: Failed after {max_retries} attempts")returnNone
backoff =2** attempt # 1s, 2s, 4s...print(f"Story {story_id}: Attempt {attempt +1} failed, retrying in {backoff}s...")await asyncio.sleep(backoff)Didukung oleh
Perhatikan kita menangkap pengecualian spesifik (aiohttp.ClientError, ValueError) alih-alih except kosong. Ini memastikan kita hanya mencoba ulang pada error yang mungkin sementara. KeyError dari kode yang buruk tidak boleh memicu retry.
Mari uji dengan campuran ID valid dan tidak valid:
asyncdefmain():asyncwith aiohttp.ClientSession()as session:asyncwith session.get(f"{HN_API}/topstories.json")as response:
story_ids =await response.json()
test_ids =[story_ids[0],99999999999, story_ids[1],88888888888, story_ids[2]]
results =await asyncio.gather(*[fetch_with_retry(session, sid)for sid in test_ids])
successful =[r for r in results if r isnotNone]print(f"\nSuccessful: {len(successful)}, Failed: {len(test_ids)-len(successful)}")
asyncio.run(main())Didukung oleh
Story 99999999999: Attempt 1 failed, retrying in 1s...
Story 88888888888: Attempt 1 failed, retrying in 1s...
Story 99999999999: Attempt 2 failed, retrying in 2s...
Story 88888888888: Attempt 2 failed, retrying in 2s...
Story 99999999999: Failed after 3 attempts
Story 88888888888: Failed after 3 attempts
Successful: 3, Failed: 2Didukung oleh
Di produksi, Anda juga akan menambahkan jitter (penundaan acak kecil) untuk mencegah beberapa permintaan gagal melakukan retry pada saat yang persis sama. Selain itu, Anda hanya akan mencoba ulang error sementara (masalah jaringan sisi server, seperti 503) sambil langsung menyerah pada error permanen (misalnya, 404 atau 401).
Penyimpanan Database Async Python dengan aiosqlite
Kita telah mengambil cerita Hacker News dengan pembatasan laju yang tepat, timeout, dan penanganan error. Sekarang mari simpan ke database.
Menggunakan pustaka database sinkron biasa seperti sqlite3 akan memblokir event loop saat kueri, menggagalkan tujuan pemrograman asinkron. Saat kode Anda menunggu database, tidak ada coroutine lain yang bisa berjalan. Untuk aplikasi async, Anda memerlukan pustaka database async.
aiosqlite membungkus sqlite3 bawaan Python dalam antarmuka async. Ia menjalankan operasi database di thread pool sehingga tidak memblokir event loop. SQLite tidak memerlukan penyiapan server—hanya sebuah berkas—jadi Anda bisa langsung menjalankan kode ini. Jika Anda baru bekerja dengan database di Python, kursus Introduction to Databases in Python membahas dasar sinkron yang menjadi landasan aiosqlite.
Menyiapkan database
Polanya seharusnya terlihat familier. Layaknya aiohttp.ClientSession, Anda menggunakan async with untuk mengelola koneksi:
import aiosqlite
asyncdefinit_db(db_path):asyncwith aiosqlite.connect(db_path)as db:await db.execute("""
CREATE TABLE IF NOT EXISTS stories (
id INTEGER PRIMARY KEY,
title TEXT,
url TEXT,
score INTEGER,
fetched_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")await db.commit()
asyncio.run(init_db("stories.db"))Didukung oleh
Fungsi-fungsi kunci:
aiosqlite.connect(path) membuka (atau membuat) berkas database.
await db.execute(sql) menjalankan pernyataan SQL.
await db.commit() menyimpan perubahan ke disk.
Menyimpan cerita
Berikut fungsi untuk menyimpan satu cerita:
asyncdefsave_story(db, story):await db.execute("INSERT OR REPLACE INTO stories (id, title, url, score) VALUES (?, ?, ?, ?)",(story["id"], story.get("title",""), story.get("url",""), story.get("score",0)))Didukung oleh
Placeholder ? mencegah injeksi SQL—jangan pernah menggunakan f-string untuk memasukkan nilai ke SQL. INSERT OR REPLACE memperbarui cerita yang sudah ada jika kita mengambilnya lagi.
Pipeline Async Lengkap di Python: Ambil dan Simpan
Sekarang mari gabungkan semua dari tutorial ini menjadi pipeline lengkap. Kita akan mengambil 20 cerita Hacker News dengan pembatasan laju dan menyimpannya ke database:
import aiohttp
import aiosqlite
import asyncio
HN_API ="https://hacker-news.firebaseio.com/v0"asyncdeffetch_story(session, story_id):asyncwith session.get(f"{HN_API}/item/{story_id}.json")as response:returnawait response.json()asyncdeffetch_story_limited(session, story_id, semaphore):asyncwith semaphore:
story =await fetch_story(session, story_id)if story:return story
returnNoneasyncdefsave_story(db, story):await db.execute("INSERT OR REPLACE INTO stories (id, title, url, score) VALUES (?, ?, ?, ?)",(story["id"], story.get("title",""), story.get("url",""), story.get("score",0)))asyncdefmain():# Initialize databaseasyncwith aiosqlite.connect("hn_stories.db")as db:await db.execute("""
CREATE TABLE IF NOT EXISTS stories (
id INTEGER PRIMARY KEY,
title TEXT,
url TEXT,
score INTEGER,
fetched_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")# Fetch storiesasyncwith aiohttp.ClientSession()as session:asyncwith session.get(f"{HN_API}/topstories.json")as response:
story_ids =await response.json()
semaphore = asyncio.Semaphore(5)
tasks =[fetch_story_limited(session, sid, semaphore)for sid in story_ids[:20]]
stories =await asyncio.gather(*tasks)# Save to databasefor story in stories:if story:await save_story(db, story)await db.commit()# Query and display
cursor =await db.execute("SELECT id, title, score FROM stories ORDER BY score DESC LIMIT 5")
rows =await cursor.fetchall()print(f"Saved {len([s for s in stories if s])} stories. Top 5 by score:")for row in rows:print(f" [{row[2]}] {row[1][:50]}")
asyncio.run(main())Didukung oleh
Saved 20 stories. Top 5 by score:
[671] Google Antigravity exfiltrates data via indirect p
[453] Trillions spent and big software projects are stil
[319] Ilya Sutskever: We're moving from the age of scali
[311] Show HN: We built an open source, zero webhooks pa
[306] FLUX.2: Frontier Visual IntelligenceDidukung oleh
Pipeline ini menggunakan pola dari setiap bagian: ClientSession untuk connection pooling, Semaphore(5) untuk pembatasan laju, gather() untuk pengambilan bersamaan, dan sekarang aiosqlite untuk penyimpanan async. Tiap komponen menangani bagiannya tanpa memblokir yang lain.
Setiap kali Anda menjalankan alur kerja ini, Anda akan menerima cerita teratas hari ini.
Kesimpulan
Tutorial ini membawa Anda dari sintaks async/await dasar hingga pipeline data lengkap. Anda mempelajari bagaimana coroutine jeda dan dilanjutkan, bagaimana event loop mengelola tugas bersamaan, dan bagaimana asyncio.gather() menjalankan beberapa operasi sekaligus. Anda menambahkan permintaan HTTP nyata dengan aiohttp, mengontrol konkurensi dengan semaphore, menangani kegagalan dengan timeout dan retry, serta menyimpan hasil ke database dengan aiosqlite.
Gunakan async saat kode Anda menunggu sistem eksternal: HTTP API, database, I/O berkas, atau soket jaringan. Untuk pekerjaan berat CPU seperti pemrosesan data atau perhitungan angka, async tidak akan membantu—lihat multiprocessing atau concurrent.futures sebagai gantinya. Untuk melangkah lebih jauh, Anda dapat menjelajahi dokumentasi asyncio dan mempertimbangkan FastAPI untuk membangun web API async.
Jika Anda ingin membangun di atas pengetahuan ini dan belajar merancang aplikasi cerdas, pastikan untuk melihat jalur karier Associate AI Engineer for Developers.
FAQ Async Python
Apa perbedaan antara pemrograman async dan sinkron di Python?⌄
Dalam pemrograman sinkron, kode dieksekusi satu baris demi satu dan menunggu setiap operasi selesai. Pemrograman async memungkinkan kode Anda memulai sebuah operasi dan beralih ke pekerjaan lain sambil menunggu, lalu melanjutkan ketika hasilnya siap. Ini dikelola oleh event loop yang beralih di antara tugas.
Kapan saya harus menggunakan pemrograman async alih-alih Python biasa?⌄
Gunakan async untuk tugas I/O-bound saat kode Anda menunggu sistem eksternal: permintaan HTTP, kueri database, operasi berkas, atau soket jaringan. Async tidak akan membantu pekerjaan CPU-bound seperti pemrosesan data atau perhitungan—untuk itu, gunakan multiprocessing atau concurrent.futures.
Mengapa saya mendapat peringatan "coroutine was never awaited"?⌄
Ini terjadi saat Anda memanggil fungsi async tanpa menggunakan await. Memanggil fungsi async seperti get_data() mengembalikan objek coroutine, bukan hasilnya. Anda harus menggunakan await get_data() untuk benar-benar mengeksekusinya dan mendapatkan nilai kembalian.
Bisakah saya menggunakan pustaka requests dengan asyncio?⌄
Tidak, pustaka requests bersifat sinkron dan memblokir event loop, menggagalkan tujuan async. Gunakan aiohttp sebagai gantinya—ini klien HTTP async yang dirancang untuk permintaan bersamaan. Ingat untuk menggunakan ulang satu ClientSession untuk connection pooling.
Bagaimana cara membatasi permintaan bersamaan agar tidak membebani sebuah API?⌄
Gunakan asyncio.Semaphore untuk mengontrol berapa banyak permintaan yang berjalan secara bersamaan. Buat semaphore dengan batas yang Anda inginkan (misalnya, asyncio.Semaphore(5)) dan bungkus tiap permintaan dengan async with semaphore. Ini memastikan hanya sebanyak itu permintaan yang "sedang berlangsung" sekaligus.
Kuasai pertanyaan wawancara basis data, dari konsep SQL dasar hingga skenario desain sistem tingkat lanjut. Panduan mendalam ini mencakup semua yang Anda perlukan untuk sukses di wawancara DBMS dan meraih peran berikutnya.
Temukan alasan mengapa Anda sebaiknya (tidak) menggunakan spaghetti plot untuk menyampaikan ketidakpastian jalur prediksi badai serta dampaknya terhadap interpretasi.
Pelajari cara mengintegrasikan kecerdasan buatan (AI) ke dalam aplikasi perangkat lunak menggunakan antarmuka pemrograman aplikasi (API) dan perpustakaan sumber terbuka. Mulailah perjalanan Anda untuk menjadi seorang Insinyur Kecerdasan Buatan (AI) hari ini!
Bangun API yang andal dan siap produksi dengan FastAPI, kuasai operasi HTTP, validasi, dan eksekusi asinkron untuk menciptakan alur data dan ML yang efisien.
Saat skrip Python Anda dengan sabar menunggu respons API, kueri database, atau operasi berkas selesai, waktu itu sering kali tidak terpakai. Dengan pemrograman async Python, kode Anda dapat menangani beberapa tugas sekaligus. Jadi ketika satu operasi menunggu, yang lain tetap berjalan, mengubah momen menganggur menjadi pekerjaan produktif dan sering kali memangkas waktu tunggu dari menit menjadi hitungan detik.
Dalam panduan ini, saya akan mengajarkan dasar-dasar pemrograman async di Python melalui mini-proyek. Anda akan melihat bagaimana coroutine, event loop, dan async I/O dapat membuat kode Anda jauh lebih responsif.
Jika Anda ingin mempelajari cara membangun web API asinkron, pastikan untuk melihat kursus tentang FastAPI.
Apa itu Pemrograman Async Python?
Dalam Python sinkron tradisional, kode Anda dieksekusi satu baris pada satu waktu. Misalnya, saat Anda memanggil sebuah API, program Anda berhenti dan menunggu respons. Jika itu memakan waktu dua detik, seluruh program Anda menganggur selama dua detik. Pemrograman asinkron memungkinkan kode Anda memulai panggilan API lalu melanjutkan tugas lain.
Saat respons tiba, kode Anda melanjutkan dari tempat semula. Alih-alih menunggu tiap operasi selesai, Anda dapat menjalankan beberapa operasi secara bersamaan. Hal ini paling penting ketika kode Anda menghabiskan waktu menunggu sistem eksternal seperti database, API, atau sistem berkas merespons.
Agar ini berhasil, sistem async Python menggunakan beberapa konsep inti:
Coroutine: Fungsi yang didefinisikan dengan
async defalih-alihdef. Fungsi ini dapat jeda dan dilanjutkan kembali, sehingga ideal untuk operasi yang melibatkan penantian.await: Kata kunci yang memberi tahu Python, "jeda coroutine ini sampai operasi ini selesai, tetapi biarkan kode lain berjalan sementara itu."Event loop: Mesin yang mengelola semua coroutine Anda, memutuskan mana yang dijalankan dan kapan beralih di antaranya.
Task: Coroutine yang dibungkus untuk eksekusi bersamaan. Anda membuatnya dengan
asyncio.create_task()untuk menjalankan beberapa operasi sekaligus.Agar tidak bingung tentang apa yang bisa (dan tidak bisa) dilakukan async programming, ingat hal ini:
Async paling efektif untuk pekerjaan I/O-bound seperti permintaan HTTP, kueri database, dan operasi berkas, saat kode Anda menunggu sistem eksternal.
Async tidak membantu untuk pekerjaan CPU-bound seperti perhitungan kompleks atau pemrosesan data, saat kode Anda aktif menghitung alih-alih menunggu.
Cara terbaik untuk memahaminya adalah dengan menulis kode async sebenarnya. Pada bagian berikutnya, Anda akan membuat fungsi async pertama Anda dan melihat persis bagaimana coroutine dan event loop bekerja bersama.
Fungsi Async Python Pertama Anda
Sebelum menulis kode async, mari lihat fungsi sinkron biasa yang menunggu sebelum melakukan sesuatu:
Fungsinya bekerja, tetapi
time.sleep(2)memblokir seluruh program Anda. Tidak ada yang bisa berjalan selama dua detik itu.Sekarang berikut versi async-nya:
Keluaran terlihat identik, tetapi ada hal berbeda yang terjadi di balik layar. Tiga perubahan membuatnya async:
async defalih-alihdefmendeklarasikan ini sebagai coroutine.await asyncio.sleep(2)alih-alihtime.sleep(2)menjeda tanpa memblokir.asyncio.run()memulai event loop dan menjalankan coroutine.Perhatikan bahwa
asyncio.sleep()sendiri adalah fungsi async, itulah sebabnya perluawait. Ini aturan kunci: setiap fungsi async harus dipanggil denganawait. Baik itu bawaan sepertiasyncio.sleep()atau buatan Anda sendiri, lupaawaitberarti fungsi itu tidak benar-benar dieksekusi.Saat ini, versi async belum terlihat lebih cepat. Itu karena kita hanya punya satu tugas. Manfaat sebenarnya muncul saat Anda menjalankan beberapa coroutine sekaligus, yang akan kita bahas di bagian berikut.
Satu hal penting lainnya: Anda tidak bisa memanggil fungsi async langsung seperti fungsi biasa. Mari kita coba:
Memanggil
greet_after_delay()mengembalikan objek coroutine, bukan hasilnya. Fungsinya tidak benar-benar berjalan. Anda memerlukanasyncio.run()atauawaituntuk mengeksekusinya di dalam fungsi lain.Cara kerja event loop
Event loop adalah mesin di balik pemrograman async. Ia mengelola coroutine Anda dan memutuskan apa yang dijalankan kapan. Berikut yang terjadi langkah demi langkah saat Anda menjalankan fungsi async
greet_after_delay():asyncio.run()membuat event loop.Event loop memulai
greet_after_delay()."Starting..." tercetak.
Mencapai await
asyncio.sleep(2)→ coroutine dijeda.Event loop memeriksa: "Ada tugas lain untuk dijalankan?" (belum ada).
2 detik berlalu, sleep selesai.
Event loop melanjutkan
greet_after_delay()."Hello!" tercetak.
Fungsi selesai → event loop keluar.
Langkah 5 adalah tempat async menjadi menarik. Dengan satu coroutine, tidak ada yang lain untuk dilakukan. Namun saat Anda memiliki banyak coroutine, event loop beralih ke pekerjaan lain sementara yang satu menunggu. Alih-alih menganggur selama tidur dua detik, ia dapat menjalankan kode lain.
Anggap event loop seperti pengatur lalu lintas. Ia tidak membuat mobil individual lebih cepat. Ia menjaga lalu lintas tetap bergerak dengan membiarkan mobil lain jalan saat satu mobil berhenti.
Kesalahan async yang umum: Lupa await
Kesalahan pemula yang umum adalah lupa
awaitsaat memanggil coroutine di dalam fungsi async lain:Tanpa
await, Anda mendapatkan objek coroutine alih-alih nilai kembalian. Python juga memperingatkan bahwa coroutine tidak pernah dieksekusi.Perbaikannya sederhana:
Saat Anda melihat
RuntimeWarningtentang coroutine yang tidak di-await, periksa bahwa Anda sudah menggunakanawaitpada setiap pemanggilan fungsi async.Tugas Async Python yang Berjalan Bersamaan
Pada bagian sebelumnya, kita mengonversi fungsi sinkron menjadi async. Namun itu tidak lebih cepat. Itu karena kita hanya menjalankan satu coroutine. Kekuatan nyata async muncul saat Anda menjalankan beberapa coroutine pada saat yang sama.
Mengapa await berurutan tetap berurutan
Anda mungkin berpikir memanggil beberapa fungsi async akan otomatis menjalankannya secara bersamaan. Namun lihat apa yang terjadi saat kita memanggil
greet_after_delay()tiga kali:Enam detik untuk tiga tugas berdurasi dua detik. Setiap
awaitmenunggu coroutine-nya selesai sebelum pindah ke baris berikutnya. Kodenya async, tetapi berjalan berurutan.Menjalankan tugas async secara bersamaan dengan asyncio.gather()
Untuk menjalankan coroutine pada saat yang sama, gunakan
asyncio.gather(). Fungsi ini menerima beberapa coroutine dan menjalankannya secara bersamaan:Dua detik alih-alih enam. Ketiga coroutine dimulai segera, tidur secara bersamaan, dan selesai bersama. Itu peningkatan 3x hanya dengan satu perubahan.
Perhatikan urutan keluaran: ketiga pesan "Starting..." tercetak sebelum pesan "Hello..." mana pun. Ini menunjukkan semua coroutine berjalan dalam jendela dua detik yang sama alih-alih saling menunggu.
asyncio.gather()mengembalikan daftar hasil dalam urutan yang sama dengan urutan Anda meneruskan coroutine. Jika coroutine Anda mengembalikan nilai, Anda dapat menampungnya:Hasil kembali dalam urutan
[10, 20, 30], sesuai urutan coroutine yang diteruskan kegather().Permintaan HTTP Async Python dengan aiohttp
Sejauh ini, kita menggunakan
asyncio.sleep()untuk mensimulasikan jeda. Sekarang mari buat permintaan HTTP nyata. Anda mungkin ingin menggunakan pustakarequests, tetapi itu tidak cocok di sini.requestsbersifat sinkron dan memblokir event loop, menggagalkan tujuan async.Sebagai gantinya, gunakan
aiohttp, klien HTTP async yang dibuat untuk tujuan ini.Pengenalan aiohttp
Berikut cara mengambil sebuah URL dengan
aiohttp:Perhatikan dua
async withbertingkat. Masing-masing mengelola sumber daya berbeda, dan memahami perannya adalah kunci menggunakanaiohttpdengan benar.Cara kerja ClientSession di aiohttp
Berikut yang terjadi langkah demi langkah saat Anda membuat permintaan dengan
aiohttp:aiohttp.ClientSession()membuat connection pool (awal kosong).session.get(url)memeriksa pool: "Ada koneksi terbuka ke host ini (server situs)?"Jika belum ada, dibuat koneksi TCP baru (protokol dasar untuk mengirim data melalui internet) dan jabat tangan SSL (penyiapan enkripsi untuk HTTPS).
Permintaan HTTP dikirim, dan kita menunggu header respons.
Objek respons memegang koneksi.
await response.text()membaca data body dari jaringan.Keluar dari
async withbagian dalam: Koneksi kembali ke pool (tetap terbuka!).Permintaan berikutnya ke host yang sama dibuat, menggunakan ulang koneksi dari pool (melewati langkah 3).
Keluar dari
async withluar: Semua koneksi dalam pool ditutup.Langkah 7 dan 8 adalah wawasan kunci. Connection pool menjaga koneksi tetap hidup di antara permintaan. Saat Anda membuat permintaan lain ke host yang sama, ia melewati proses TCP dan jabat tangan SSL sepenuhnya.
Ini penting karena membangun koneksi baru itu lambat. Jabat tangan TCP membutuhkan satu perjalanan bolak-balik ke server. Jabat tangan SSL membutuhkan dua lagi. Bergantung pada latensi, itu 100–300 ms sebelum Anda bahkan mengirim byte data pertama.
Menggunakan sesi bersama untuk semua permintaan
Sekarang Anda bisa melihat mengapa membuat sesi baru untuk setiap permintaan adalah masalah:
Setiap pemanggilan
fetch_bad()membuat sesi baru dengan pool kosong. Setiap permintaan membayar biaya jabat tangan penuh, meskipun semuanya ke host yang sama.Solusinya adalah membuat satu sesi dan meneruskannya ke fungsi fetch Anda:
Dengan sesi bersama, permintaan pertama membangun koneksi, dan sembilan permintaan berikutnya menggunakannya kembali. Satu jabat tangan alih-alih sepuluh.
Contoh permintaan HTTP async: Mengambil data Hacker News
Mari praktikkan ini dengan API Hacker News. API ini sempurna untuk mendemonstrasikan perilaku asinkron karena mengambil cerita memerlukan beberapa permintaan. Jika Anda baru bekerja dengan REST API di Python, lihat Python APIs: A Guide to Building and Using APIs untuk konsep dasar.
Struktur API Hacker News:
https://hacker-news.firebaseio.com/v0/topstories.jsonmengembalikan daftar ID cerita (hanya angka)https://hacker-news.firebaseio.com/v0/item/{id}.jsonmengembalikan detail untuk satu ceritaUntuk mendapatkan 10 cerita, Anda memerlukan 11 permintaan: satu untuk daftar ID, lalu satu untuk tiap cerita. Di sinilah pemrograman async bersinar.
Pertama, mari lihat apa yang dikembalikan API jika kita mencoba mengambil cerita pertama:
API mengembalikan 500 ID cerita, dan setiap cerita memiliki field seperti
title,url,score, danby(penulis).Mengambil banyak hasil secara berurutan vs. bersamaan
Sekarang mari ambil 10 cerita secara berurutan:
Sekarang mari ambil cerita yang sama secara bersamaan:
Versi bersamaan 3,5x lebih cepat. Alih-alih menunggu tiap permintaan selesai sebelum memulai berikutnya, semua 10 permintaan berjalan pada saat yang sama. Di sinilah pemrograman async memberikan hasil nyata pada I/O jaringan.
Penanganan Error dan Pembatasan Laju pada Async Python
Saat mengambil data secara bersamaan, beberapa hal bisa salah. Anda bisa membebani server dengan terlalu banyak permintaan. Beberapa permintaan bisa menggantung selamanya. Yang lain mungkin langsung gagal. Dan ketika kegagalan terjadi, Anda memerlukan strategi pemulihan.
Bagian ini membahas tiap kekhawatiran sesuai urutan terjadinya: mengontrol berapa banyak permintaan yang keluar, menetapkan batas waktu, menangani kegagalan, dan mencoba ulang saat masuk akal. Jika Anda perlu penyegaran tentang dasar-dasar penanganan pengecualian Python, lihat Exception & Error Handling in Python. Kita akan menggunakan setelan dasar ini sepanjang pembahasan:
Pembatasan laju dengan semaphore
Pada bagian sebelumnya, kita menembakkan 10 permintaan sekaligus. Itu berjalan baik. Namun bagaimana saat Anda perlu mengambil 500 cerita? Atau meng-crawl 10.000 halaman?
Sebagian besar API menerapkan rate limit. Mereka mungkin mengizinkan 10 permintaan per detik, atau 100 koneksi bersamaan. Jika melampaui batas, Anda akan diblokir, diperlambat, atau dibanned. Bahkan jika API tidak menerapkan batas, menembakkan ribuan permintaan sekaligus bisa membebani sistem Anda sendiri atau server.
Anda butuh cara untuk mengontrol berapa banyak permintaan yang “sedang berlangsung” pada momen tertentu. Itulah peran semaphore.
Semaphore bekerja seperti sistem izin. Bayangkan Anda memiliki tiga izin. Tugas mana pun yang ingin membuat permintaan harus terlebih dahulu mendapatkan izin. Saat selesai, ia mengembalikan izin, sehingga permintaan baru bisa menggunakannya. Jika tidak ada izin yang tersedia, tugas menunggu sampai ada yang bebas.
Begini jalannya dengan 3 izin dan 4 atau lebih tugas:
Tersedia tiga izin.
Tugas A mengambil satu izin (tersisa 2), memulai permintaan.
Tugas B mengambil satu izin (tersisa 1), memulai permintaan.
Tugas C mengambil satu izin (tersisa 0), memulai permintaan.
Tugas D ingin izin, tetapi tidak ada yang tersedia—ia menunggu.
Tugas A selesai, mengembalikan izinnya (tersedia 1).
Tugas D mengambil izin tersebut dan memulai permintaan.
Ini berlanjut hingga semua tugas selesai.
Penungguan pada langkah 5 efisien. Tugas tidak berputar dalam loop memeriksa "apakah ada izin yang bebas?". Ia disuspensi dan membiarkan kode lain berjalan. Event loop membangunkannya hanya saat izin tersedia.
Sekarang lihat kodenya. Dalam asyncio, Anda membuat semaphore dengan
asyncio.Semaphore(n), di mananadalah jumlah izin. Untuk menggunakannya, bungkus kode Anda dalamasync with semaphore:. Ini memperoleh izin saat memasuki blok dan otomatis melepaskannya saat keluar:Mari bandingkan mengambil 30 cerita dengan dan tanpa semaphore:
Versi semaphore lebih lambat karena memproses permintaan dalam batch berisi lima. Namun itulah konsekuensinya: Anda mengorbankan kecepatan demi perilaku yang lebih dapat diprediksi dan ramah server.
Satu hal yang perlu dicatat: semaphore membatasi permintaan yang bersamaan, bukan permintaan per satuan waktu.
Semaphore(10)berarti "maksimal 10 permintaan sedang berlangsung sekaligus," bukan "10 permintaan per detik." Jika Anda membutuhkan pembatasan berbasis waktu yang ketat (seperti tepat 10 permintaan per detik), Anda bisa menggabungkan semaphore dengan jeda antar batch, atau gunakan pustaka sepertiaiolimiter.Timeout dengan asyncio.wait_for()
Bahkan dengan konkurensi terkontrol, permintaan individual bisa menggantung. Server mungkin menerima koneksi Anda tetapi tidak pernah merespons. Tanpa timeout, program Anda menunggu tanpa batas.
Fungsi
asyncio.wait_for()membungkus coroutine apa pun dengan tenggat waktu. Anda memberinya coroutine dantimeoutdalam detik. Jika operasi tidak selesai tepat waktu, fungsi ini melemparasyncio.TimeoutError:Saat timeout habis,
wait_for()membatalkan coroutine. Anda bisa menangkapTimeoutErrordan memutuskan apa yang harus dilakukan: lewati permintaan, kembalikan nilai bawaan, atau coba lagi.Untuk permintaan bersamaan, bungkus masing-masing secara individual. Berikut helper yang mengembalikan kamus error alih-alih melempar error:
Saat coroutine dibatalkan (oleh timeout atau alasan lain), Python melempar
asyncio.CancelledErrordi dalamnya. Jika coroutine Anda memegang sumber daya seperti file handle atau koneksi, gunakantry/finallyuntuk memastikan pembersihan tetap terjadi bahkan saat pembatalan:Penanganan error dengan asyncio.gather()
Timeout menangkap permintaan yang lambat. Namun beberapa permintaan gagal seketika dengan error. Mari lihat apa yang terjadi saat satu permintaan dalam satu batch gagal.
Pertama, kita butuh versi
fetch_story()yang melempar pengecualian pada ID tidak valid:Sekarang mari ambil empat cerita valid plus satu ID tidak valid:
Dengan satu ID tidak valid, kita kehilangan keempat hasil yang berhasil. Secara bawaan,
gather()menggunakan perilaku fail-fast: satu pengecualian membatalkan semuanya dan meneruskan ke atas.Untuk mempertahankan hasil parsial, tambahkan
return_exceptions=True. Ini mengubah perilakugather(): alih-alih melempar pengecualian, ia mengembalikannya sebagai item dalam daftar hasil bersama nilai yang berhasil:Pemeriksaan
isinstance(result, Exception)memungkinkan Anda memisahkan hasil yang berhasil dari error. Anda kemudian dapat memproses yang berhasil dan mencatat atau mencoba lagi yang gagal.Logika retry dengan exponential backoff
Beberapa kegagalan bersifat sementara. Server mungkin kelebihan beban sesaat, atau gangguan jaringan menjatuhkan koneksi Anda. Untuk kasus ini, mencoba ulang masuk akal.
Namun mencoba ulang segera dapat memperburuk keadaan. Jika server sedang kesulitan, membombardirnya dengan retry akan menambah masalah. Exponential backoff menyelesaikan ini dengan menunggu lebih lama di antara tiap percobaan.
Pola ini menggunakan
2 ** attemptuntuk menghitung waktu tunggu: percobaan 0 menunggu satu detik (2⁰), percobaan 1 menunggu dua detik (2¹), percobaan 2 menunggu empat detik (2²), dan seterusnya. Ini memberi server waktu pemulihan yang makin panjang:Perhatikan kita menangkap pengecualian spesifik (
aiohttp.ClientError,ValueError) alih-alihexceptkosong. Ini memastikan kita hanya mencoba ulang pada error yang mungkin sementara.KeyErrordari kode yang buruk tidak boleh memicu retry.Mari uji dengan campuran ID valid dan tidak valid:
Di produksi, Anda juga akan menambahkan jitter (penundaan acak kecil) untuk mencegah beberapa permintaan gagal melakukan retry pada saat yang persis sama. Selain itu, Anda hanya akan mencoba ulang error sementara (masalah jaringan sisi server, seperti 503) sambil langsung menyerah pada error permanen (misalnya, 404 atau 401).
Penyimpanan Database Async Python dengan aiosqlite
Kita telah mengambil cerita Hacker News dengan pembatasan laju yang tepat, timeout, dan penanganan error. Sekarang mari simpan ke database.
Menggunakan pustaka database sinkron biasa seperti
sqlite3akan memblokir event loop saat kueri, menggagalkan tujuan pemrograman asinkron. Saat kode Anda menunggu database, tidak ada coroutine lain yang bisa berjalan. Untuk aplikasi async, Anda memerlukan pustaka database async.aiosqlitemembungkussqlite3bawaan Python dalam antarmuka async. Ia menjalankan operasi database di thread pool sehingga tidak memblokir event loop. SQLite tidak memerlukan penyiapan server—hanya sebuah berkas—jadi Anda bisa langsung menjalankan kode ini. Jika Anda baru bekerja dengan database di Python, kursus Introduction to Databases in Python membahas dasar sinkron yang menjadi landasanaiosqlite.Menyiapkan database
Polanya seharusnya terlihat familier. Layaknya
aiohttp.ClientSession, Anda menggunakanasync withuntuk mengelola koneksi:Fungsi-fungsi kunci:
aiosqlite.connect(path)membuka (atau membuat) berkas database.await db.execute(sql)menjalankan pernyataan SQL.await db.commit()menyimpan perubahan ke disk.Menyimpan cerita
Berikut fungsi untuk menyimpan satu cerita:
Placeholder
?mencegah injeksi SQL—jangan pernah menggunakan f-string untuk memasukkan nilai ke SQL.INSERT OR REPLACEmemperbarui cerita yang sudah ada jika kita mengambilnya lagi.Pipeline Async Lengkap di Python: Ambil dan Simpan
Sekarang mari gabungkan semua dari tutorial ini menjadi pipeline lengkap. Kita akan mengambil 20 cerita Hacker News dengan pembatasan laju dan menyimpannya ke database:
Pipeline ini menggunakan pola dari setiap bagian:
ClientSessionuntuk connection pooling,Semaphore(5)untuk pembatasan laju,gather()untuk pengambilan bersamaan, dan sekarangaiosqliteuntuk penyimpanan async. Tiap komponen menangani bagiannya tanpa memblokir yang lain.Setiap kali Anda menjalankan alur kerja ini, Anda akan menerima cerita teratas hari ini.
Kesimpulan
Tutorial ini membawa Anda dari sintaks
async/awaitdasar hingga pipeline data lengkap. Anda mempelajari bagaimana coroutine jeda dan dilanjutkan, bagaimana event loop mengelola tugas bersamaan, dan bagaimanaasyncio.gather()menjalankan beberapa operasi sekaligus. Anda menambahkan permintaan HTTP nyata denganaiohttp, mengontrol konkurensi dengan semaphore, menangani kegagalan dengan timeout dan retry, serta menyimpan hasil ke database denganaiosqlite.Gunakan async saat kode Anda menunggu sistem eksternal: HTTP API, database, I/O berkas, atau soket jaringan. Untuk pekerjaan berat CPU seperti pemrosesan data atau perhitungan angka, async tidak akan membantu—lihat
multiprocessingatauconcurrent.futuressebagai gantinya. Untuk melangkah lebih jauh, Anda dapat menjelajahi dokumentasi asyncio dan mempertimbangkan FastAPI untuk membangun web API async.Jika Anda ingin membangun di atas pengetahuan ini dan belajar merancang aplikasi cerdas, pastikan untuk melihat jalur karier Associate AI Engineer for Developers.