Resource seperti koneksi database dan session HTTP kini punya siklus hidup yang sendirinya menunggu: membuka koneksi menunggu handshake jaringan, menutup koneksi menunggu pesan penutup terkirim. Statement with pada Chapter Context Manager dan with tidak bisa membantu karena __enter__ dan __exit__-nya tidak boleh await. Untuk itu Python menyediakan async with beserta protokol pasangannya, plus asyncio.Lock untuk melindungi state yang dipakai bersama antar-task.
Protokol aenter dan aexit #
async with bekerja lewat dua special method: __aenter__ untuk membuka resource dan __aexit__ untuk membersihkannya — keduanya dideklarasikan sebagai coroutine dan otomatis di-await oleh statement. Sebuah class cukup mengimplementasikan keduanya:
import asyncio
class KoneksiDatabase:
async def __aenter__(self):
print("membuka koneksi")
await asyncio.sleep(0.1)
return self
async def __aexit__(self, exc_type, exc, tb):
print("menutup koneksi")
await asyncio.sleep(0.1)
async def query(self, sql):
await asyncio.sleep(0.05)
return [f"baris-{i}" for i in range(3)]
async def main():
async with KoneksiDatabase() as koneksi:
hasil = await koneksi.query("SELECT * FROM pengguna")
print(hasil)
asyncio.run(main())Output yang Dihasilkan:
membuka koneksi
['baris-0', 'baris-1', 'baris-2']
menutup koneksiMari kita telaah apa yang terjadi pada baris-baris kode di atas:
- Baris 4 (
async def __aenter__(self):): dipanggil saat masuk blokasync with. Setelah handshake disimulasikan selesai, method mengembalikanself— object inilah yang diikat ke namakoneksi. - Baris 9 (
async def __aexit__(self, exc_type, exc, tb):): dipanggil saat blok ditinggalkan, baik normal maupun karena exception. Tiga parameternya berisi informasi exception (atauNonejika blok selesai normal), dan method ini bolehawaituntuk proses pembersihan jaringan. - Baris 18 (
async with KoneksiDatabase() as koneksi:): interpreter men-await__aenter__, menjalankan isi blok, lalu men-await__aexit__— urutan ketiga baris output mencerminkan persis siklus tersebut.
Dekorator @asynccontextmanager #
Menulis class utuh terasa berlebihan untuk setup-teardown sederhana. Module contextlib menyediakan dekorator @asynccontextmanager supaya cleanup ditulis seperti menulis generator: bagian sebelum yield adalah __aenter__, bagian setelahnya adalah __aexit__:
import asyncio
from contextlib import asynccontextmanager
@asynccontextmanager
async def koneksi_database():
print("membuka koneksi")
await asyncio.sleep(0.1)
try:
yield "koneksi aktif"
finally:
print("menutup koneksi")
async def main():
async with koneksi_database() as koneksi:
print("menggunakan", koneksi)
asyncio.run(main())Output yang Dihasilkan:
membuka koneksi
menggunakan koneksi aktif
menutup koneksiNilai yang di-yield ("koneksi aktif") menjadi hasil as koneksi, dan blok finally menjamin pesan penutup tetap terkirim meskipun isi blok async with menaikkan exception. Gunakan bentuk class ketika object perlu menyimpan state dan method tambahan seperti query(); gunakan dekorator ketika hanya perlu sepasang pembuka-penutup.
asyncio.Lock untuk Melindungi State Bersama #
Meski berjalan di satu thread, dua task masih bisa saling menginjak ketika sebuah operasi “baca-olah-tulis” diselingi await — di titik await task lain bebas maju dan membaca nilai setengah jadi. asyncio.Lock menjamin hanya satu coroutine yang berada di critical section pada satu waktu:
import asyncio
penghitung = 0
async def naikkan_tanpa_lock():
global penghitung
for _ in range(1000):
nilai = penghitung
await asyncio.sleep(0)
penghitung = nilai + 1
async def naikkan_dengan_lock(kunci):
global penghitung
for _ in range(1000):
async with kunci:
nilai = penghitung
await asyncio.sleep(0)
penghitung = nilai + 1
async def main():
global penghitung
await asyncio.gather(naikkan_tanpa_lock(), naikkan_tanpa_lock())
print("hasil tanpa lock:", penghitung)
penghitung = 0
kunci = asyncio.Lock()
await asyncio.gather(naikkan_dengan_lock(kunci), naikkan_dengan_lock(kunci))
print("hasil dengan lock:", penghitung)
asyncio.run(main())Output yang Dihasilkan:
hasil tanpa lock: 1000
hasil dengan lock: 2000Kedua versi melakukan total 2000 penambahan. Tanpa lock, hasilnya hanya 1000 karena await asyncio.sleep(0) di tengah baca-tulis memberi task lain kesempatan membaca nilai lama, lalu keduanya menulis hasil perhitungan yang sama — pembaruan yang hilang. Dengan async with kunci:, bagian baca-tulis menjadi tidak bisa disisipi, dan hasilnya utuh 2000.
Lock hanya dibutuhkan ketika operasi baca-olah-tulis kita benar-benar melintasi await; operasi yang berada di antara dua await tidak mungkin disisipi task lain. asyncio juga menyediakan alat koordinasi lain dengan gaya pemakaian serupa: asyncio.Semaphore membatasi jumlah task yang masuk bersamaan, asyncio.Event menandai bahwa sebuah kondisi telah terjadi, dan asyncio.Condition menggabungkan keduanya.
Pola async with ini juga fondasi penggunaan nyata seperti connection pool: pool dibuka sekali dengan async with, banyak query() berjalan lewat koneksi-koneksi yang dikelola pool, dan seluruh koneksi ditutup rapi di akhir — pola yang kita lihat lagi pada Chapter Async Network I/O dan client HTTP async pada Chapter Menggunakan HTTP Client.