# Asyncio di Python: async/await, TaskGroup, thread, multiprocessing, dan GIL

URL: https://skillaria.top/id/guides/python/asyncio-python

Asyncio adalah pustaka bawaan Python untuk kode asinkron: fungsi yang dideklarasikan dengan async def (korutin) bisa dijeda dengan await, dan selama menunggu, event loop menjalankan tugas lain, sehingga ratusan permintaan jaringan bisa berjalan bersamaan dalam satu thread. Untuk menunggu jaringan dan disk cocok asyncio atau thread (threading), sedangkan untuk perhitungan berat digunakan proses (multiprocessing), karena GIL membuat thread pada CPython biasa tidak bisa menghitung secara paralel.

Asyncio adalah pustaka bawaan Python untuk kode asinkron. Fungsi yang dideklarasikan dengan `async def` bisa dijeda pada `await`, dan selagi satu fungsi menunggu respons jaringan, event loop menjalankan yang lain. Dengan cara ini, ratusan permintaan bisa berjalan bersamaan dalam satu thread. Namun asyncio hanyalah salah satu dari tiga alat: ada juga thread (`threading`) dan proses (`multiprocessing`). Berikut dibahas cara kerja asyncio python, pilihan yang tepat untuk setiap tugas, dan peran GIL.

## Menunggu dan menghitung: dua hal yang berbeda

Pembedaan ini wajib dipahami dulu. Program menjadi lambat karena salah satu dari dua alasan:

- **I/O-bound** — menunggu input/output: respons API, database, pembacaan disk. Prosesor dalam kondisi menganggur.
- **CPU-bound** — menghitung: memproses gambar, menjalankan loop dengan jutaan data. Prosesor bekerja penuh.

Untuk menunggu, asyncio atau thread cocok: selagi satu operasi menunggu, operasi lain dijalankan. Untuk perhitungan dibutuhkan paralelisme sungguhan di beberapa core — yaitu proses. Kesalahan utamanya adalah tertukar: thread tidak mempercepat perhitungan murni dengan Python, sedangkan proses hanya untuk tiga permintaan HTTP adalah «meriam untuk menembak nyamuk».

## Apa itu asyncio: korutin dan event loop

Kode asinkron dibangun di atas dua kata. `async def` mendeklarasikan korutin — fungsi yang bisa dijeda. Memanggilnya tidak menjalankan badan fungsi, melainkan membuat objek korutin. `await` di dalamnya berarti: «di sini saya menunggu, sementara itu kerjakan tugas lain». Semuanya dikendalikan oleh event loop, yang dijalankan lewat `asyncio.run()`.

```
import asyncio

async def fetch_rate(currency):
    await asyncio.sleep(1)          # simulasi permintaan ke API
    return {"USD": 92.5, "EUR": 100.1, "CNY": 12.7}[currency]

async def main():
    rate = await fetch_rate("USD")
    print(rate)

asyncio.run(main())  # 92.5
```

`asyncio.sleep()` adalah jeda asinkron: ia mengembalikan kendali ke event loop. Sebaliknya, `time.sleep()` di dalam korutin akan memblokir seluruh event loop, dan tugas lain ikut berhenti.

### Berurutan atau bersamaan

Jika `await` ditulis di dalam perulangan, permintaan berjalan satu per satu — tiga mata uang memakan tiga detik. Agar berjalan bersamaan, jalankan sebagai task.

```
import asyncio, time

async def main():
    start = time.perf_counter()
    for cur in ("USD", "EUR", "CNY"):
        await fetch_rate(cur)                       # satu per satu
    print(f"{time.perf_counter() - start:.1f} dtk")   # 3.0 dtk

asyncio.run(main())
```

## TaskGroup: jalankan bersama dan tunggu semuanya

Cara modern (Python 3.11+) adalah grup task. Di dalam blok `async with`, setiap task langsung dijalankan lewat `tg.create_task()`, dan keluar dari blok terjadi ketika semuanya selesai.

```
async def main():
    start = time.perf_counter()
    async with asyncio.TaskGroup() as tg:
        tasks = {cur: tg.create_task(fetch_rate(cur)) for cur in ("USD", "EUR", "CNY")}
    rates = {cur: t.result() for cur, t in tasks.items()}
    print(rates, f"{time.perf_counter() - start:.1f} dtk")   # ... 1.0 dtk

asyncio.run(main())
```

TaskGroup bersifat ketat: jika satu task gagal, task lain dibatalkan, dan semua error dikumpulkan dalam `ExceptionGroup`. Penangkapannya memakai `except*`:

```
async def main():
    try:
        async with asyncio.TaskGroup() as tg:
            tg.create_task(fetch_rate("USD"))
            tg.create_task(fetch_rate("XXX"))   # KeyError
    except* KeyError as eg:
        print("mata uang tidak dikenal:", eg.exceptions)
```

### asyncio.gather dan create_task

Sebelum TaskGroup, standarnya adalah `asyncio.gather()`: ia menjalankan korutin secara bersamaan dan mengembalikan hasilnya sesuai urutan yang sama. Pola ini masih banyak ditemui di kode lama.

```
async def main():
    usd, eur, cny = await asyncio.gather(
        fetch_rate("USD"), fetch_rate("EUR"), fetch_rate("CNY")
    )
    # dengan return_exceptions=True, error dikembalikan dalam list, tidak menghentikan semuanya
```

`asyncio.create_task()` menjalankan korutin di latar belakang tanpa grup. Referensi ke task harus disimpan, jika tidak pengumpul sampah bisa menghapusnya, dan error-nya tidak akan terlihat siapa pun. Karena itu, untuk proyek baru TaskGroup lebih aman.

### Pembatalan task dan timeout

```
async def main():
    task = asyncio.create_task(fetch_rate("USD"))
    task.cancel()                     # di dalam task akan muncul CancelledError
    try:
        await task
    except asyncio.CancelledError:
        print("dibatalkan")

    try:
        async with asyncio.timeout(0.5):   # Python 3.11+
            await fetch_rate("EUR")
    except TimeoutError:
        print("terlalu lama")
```

Di versi lama, sebagai ganti `asyncio.timeout()` dipakai `asyncio.wait_for(coro, timeout=0.5)`. Ada juga `asyncio.wait()`, yang mengembalikan himpunan task yang selesai dan yang belum, serta bisa menunggu yang pertama (`return_when=asyncio.FIRST_COMPLETED`).

Di Python 3.15, seperti ditunjukkan dalam kursus, TaskGroup mendapat metode `tg.cancel()` yang membatalkan semua task dalam grup tanpa pengecualian. Ini menyelesaikan skenario «siapa yang pertama»: tanyakan tiga layanan cermin, ambil jawaban pertama, batalkan sisanya. Sebelumnya, hal ini harus dilakukan dengan exception sinyal buatan sendiri atau membatalkan task secara manual.

## Sinkronisasi: Semaphore, Lock, Event, Queue

Asyncio punya primitif sendiri yang bekerja dengan `await`. Yang paling berguna adalah `Semaphore`: membatasi jumlah operasi yang berjalan bersamaan, agar API pihak lain tidak kebanjiran seribu permintaan.

```
sem = asyncio.Semaphore(10)

async def limited(url):
    async with sem:              # paling banyak 10 sekaligus
        await asyncio.sleep(0.1) # di sini seharusnya ada permintaan
        return url
```

- `asyncio.Lock` — hanya satu korutin yang berada di bagian kritis.
- `asyncio.Event` — satu korutin menunggu dengan `await event.wait()`, yang lain memberi sinyal lewat `event.set()`.
- `asyncio.Queue` — antrean produser–konsumen: `await q.put()`, `await q.get()`, `q.task_done()`.

## Jebakan: lupa await

Kesalahan yang paling sering terjadi adalah memanggil korutin tanpa `await`:

```
async def main():
    rate = fetch_rate("USD")    # lupa await
    print(rate * 2)             # TypeError
# RuntimeWarning: coroutine 'fetch_rate' was never awaited
```

Python tidak menjalankan fungsinya, hanya membuat objek korutin. Jika di log muncul «was never awaited», cari `await` yang terlewat. Kesalahan kedua yang umum adalah pemanggilan yang memblokir (`time.sleep`, `requests.get`) di dalam korutin. Untuk HTTP gunakan pustaka asinkron (httpx, aiohttp), dan kode sinkron yang tidak bisa dihindari dipindahkan ke thread dengan `await asyncio.to_thread(func, arg)`.

## Multithreading di Python: modul threading

Thread adalah jalur eksekusi independen di dalam satu proses; thread-thread berbagi memori yang sama. Untuk menunggu jaringan, thread bekerja dengan baik dan tidak mengharuskan kode ditulis ulang dengan async.

```
import threading, time

def download(name):
    time.sleep(1)               # menunggu jaringan
    print(name, "selesai")

threads = [threading.Thread(target=download, args=(n,)) for n in ("a", "b", "c")]
for t in threads:
    t.start()
for t in threads:
    t.join()                    # semuanya sekitar 1 detik
```

Memori bersama adalah keuntungan sekaligus sumber masalah: jika dua thread mengubah variabel yang sama, dibutuhkan `threading.Lock`. Biasanya lebih mudah tidak mengatur thread secara manual, tetapi memakai pool.

### ThreadPoolExecutor

`concurrent.futures.ThreadPoolExecutor` membuat pool thread dan membagikan tugas kepadanya. `map` mengembalikan hasil sesuai urutan asal.

```
from concurrent.futures import ThreadPoolExecutor
import time

def fetch(url):
    time.sleep(1)
    return f"{url}: 200"

urls = [f"https://example.com/{i}" for i in range(5)]
with ThreadPoolExecutor(max_workers=5) as pool:
    for result in pool.map(fetch, urls):
        print(result)           # semuanya sekitar 1 detik
```

## Apa itu GIL dan mengapa thread tidak mempercepat perhitungan

GIL (Global Interpreter Lock) adalah kunci global interpreter CPython. Pada setiap saat, hanya satu thread yang menjalankan bytecode Python. Selama thread menunggu I/O, ia melepaskan GIL, jadi thread berguna untuk jaringan. Namun pada perhitungan murni, empat thread tidak lebih cepat daripada satu: mereka bergantian memegang kunci.

```
from concurrent.futures import ThreadPoolExecutor

def heavy(n):
    return sum(i * i for i in range(n))

with ThreadPoolExecutor(4) as pool:
    list(pool.map(heavy, [5_000_000] * 4))   # kira-kira sama seperti berurutan
```

Pustaka berbasis C (NumPy dan sejenisnya) sering melepaskan GIL selama operasi berat, sehingga untuk pustaka tersebut thread memang memberi percepatan.

## multiprocessing: paralelisme sungguhan

Setiap proses memiliki interpreter dan GIL sendiri, sehingga proses-proses bisa menghitung di core yang berbeda secara bersamaan. Harganya adalah memori terpisah: data antarproses dikirim dengan serialisasi (pickle), dan memulai proses lebih mahal daripada memulai thread.

```
from concurrent.futures import ProcessPoolExecutor

def heavy(n):
    return sum(i * i for i in range(n))

if __name__ == "__main__":      # wajib di Windows dan macOS
    with ProcessPoolExecutor() as pool:
        print(list(pool.map(heavy, [5_000_000] * 4)))
```

Tanpa pemeriksaan `if __name__ == "__main__"`, proses anak akan mulai membuat proses baru saat modul diimpor. Ada juga modul tingkat rendah `multiprocessing` dengan `Process`, `Pool`, dan `Queue`, tetapi `ProcessPoolExecutor` lebih sederhana dan memakai antarmuka yang sama dengan pool thread.

## Memilih: asyncio, threading, atau multiprocessing

| Tugas | Alat | Alasan |
| --- | --- | --- |

| Mengunduh 200 halaman, banyak permintaan API | asyncio (httpx/aiohttp) atau ThreadPoolExecutor | menunggu jaringan, prosesor tetap bebas |

| Beberapa permintaan di dalam program yang sudah asinkron | TaskGroup | kode sudah berbasis async |

| Pustaka sinkron tanpa versi async | thread, `asyncio.to_thread` | kode tidak perlu ditulis ulang |

| Perhitungan berat dengan Python murni | ProcessPoolExecutor | melewati GIL, memakai semua core |

## Python tanpa GIL dan subinterpreter: yang baru

Sejak Python 3.13 tersedia build tanpa GIL (free-threading, PEP 703) — ditandai huruf t di nama, misalnya `python3.15t`. Di 3.13 ia masih eksperimental, sedangkan sejak 3.14 (PEP 779) didukung resmi. Di sana thread benar-benar menghitung paralel di core yang berbeda. Namun secara bawaan Anda tetap mendapat Python biasa dengan GIL: kode single-thread di build free-threaded sedikit lebih lambat, dan belum semua pustaka siap untuk itu.

```
import sys
print(sys._is_gil_enabled())   # False di build free-threaded
```

Jalur lain untuk perhitungan paralel muncul di 3.14 (PEP 734) — subinterpreter di modul `concurrent.interpreters`: beberapa interpreter terisolasi dalam satu proses, masing-masing dengan GIL sendiri. Cara termudah memakainya adalah `InterpreterPoolExecutor`, yang tampak sama dengan pool thread. Dalam pengukuran kursus, empat tugas berat selesai dalam 0,39 dtk dibanding 1,03 dtk dengan satu thread.

## Ringkasan

Asyncio python adalah alat untuk tugas menunggu: korutin dengan `async def`, jeda dengan `await`, dan menjalankan secara bersamaan lewat TaskGroup atau `gather`. Thread menyelesaikan masalah yang sama tanpa perlu menulis ulang kode menjadi async, sedangkan untuk perhitungan berat dibutuhkan proses, karena GIL mencegah thread pada CPython biasa menghitung secara paralel. Free-threading dan subinterpreter perlahan mengubah gambaran ini, tetapi aturan dasarnya tetap berlaku: menunggu — asyncio atau thread; menghitung — proses. Di kursus, semua ini dipraktikkan: pencatat pengeluaran mengambil kurs mata uang secara paralel dalam satu detik, bukan tiga detik.

## FAQ

**Bisakah memanggil fungsi async dari kode biasa?**

Bisa, lewat asyncio.run(coro()): ia membuat event loop, menjalankan korutin, lalu menutup loop. Di dalam loop yang sudah berjalan, asyncio.run tidak boleh dipanggil; di sana digunakan await.

**Apakah asyncio berjalan di banyak thread?**

Tidak. Event loop menjalankan semua korutin dalam satu thread dan berpindah hanya pada titik await. Karena itu race condition lebih jarang dibanding thread, tetapi satu operasi yang memblokir akan menghentikan semuanya.

**Perlukah memasang asyncio lewat pip?**

Tidak. asyncio sudah termasuk pustaka standar Python 3; cukup tulis import asyncio. Paket asyncio di PyPI sudah usang dan tidak perlu dipasang.

**Bagaimana menjalankan fungsi berat dari asyncio tanpa memblokir loop?**

Gunakan loop.run_in_executor dengan ProcessPoolExecutor untuk perhitungan, atau asyncio.to_thread untuk I/O yang memblokir; event loop akan terus melayani task lain.

**Apa bedanya korutin dan task?**

Korutin adalah objek yang tidak melakukan apa pun sampai ditunggu dengan await. Task membungkus korutin dan langsung menjadwalkannya di event loop, sehingga ia berjalan bersamaan dengan kode lain.


---
Source: Skillaria.Top (https://skillaria.top)
