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:

python
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:

text
membuka koneksi
['baris-0', 'baris-1', 'baris-2']
menutup koneksi

Mari kita telaah apa yang terjadi pada baris-baris kode di atas:

  1. Baris 4 (async def __aenter__(self):): dipanggil saat masuk blok async with. Setelah handshake disimulasikan selesai, method mengembalikan self — object inilah yang diikat ke nama koneksi.
  2. Baris 9 (async def __aexit__(self, exc_type, exc, tb):): dipanggil saat blok ditinggalkan, baik normal maupun karena exception. Tiga parameternya berisi informasi exception (atau None jika blok selesai normal), dan method ini boleh await untuk proses pembersihan jaringan.
  3. 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__:

python
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:

text
membuka koneksi
menggunakan koneksi aktif
menutup koneksi

Nilai 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:

python
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:

text
hasil tanpa lock: 1000
hasil dengan lock: 2000

Kedua 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.