8.15. Jebakan Umum

Pola-pola yang membuat asyncio nyaman digunakan -- tidak ada preemption, awaits yang eksplisit -- juga memberinya serangkaian jebakan tersendiri. Halaman ini adalah katalog dari jebakan yang cukup sering muncul sehingga layak untuk diketahui.

8.15.1. Lupa menggunakan await

Memanggil fungsi async def menghasilkan objek coroutine. Fungsi tersebut tidak menjalankan isi fungsinya. Untuk benar-benar mengeksekusinya, coroutine harus di-awaited atau dibungkus dalam sebuah task:

async def main():
    send_request()              # bug: returns the coroutine, does nothing
    await send_request()        # right: run it to completion
    asyncio.create_task(send_request())  # right: run it concurrently

Bug ini bersifat diam -- objek coroutine dibuat, dibuang, dan tidak pernah dieksekusi. Aplikasi berjalan seolah-olah semuanya berjalan dengan baik. MicroPython terkadang mencatat peringatan bahwa sebuah coroutine tidak pernah di-await; terkadang tidak. Periksa setiap titik pemanggilan yang terlihat seperti pemanggilan fungsi untuk memastikan tidak ada awaits yang hilang.

8.15.2. Loop ketat tanpa await

Coroutine yang berjalan dalam loop dan tidak pernah awaits memonopoli event loop. Tidak ada task lain yang bisa berkembang sampai loop keluar atau menyerahkan kontrol:

async def counter():
    n = 0
    while True:
        n += 1               # bug: starves the loop

Solusinya adalah yield di dalam loop -- biasanya await asyncio.sleep_ms(0) -- sehingga task lain yang siap mendapat kesempatan untuk berjalan. Pekerjaan yang berat secara komputasi juga termasuk dalam pola ini: loop pemrosesan citra yang berjalan selama ratusan milidetik per iterasi harus yield setidaknya sekali per iterasi agar bagian lain dari program tidak macet.

8.15.3. Menelan CancelledError

Halaman pembatalan sudah membahas ini secara detail. Diulang di sini karena ini adalah penyebab paling umum dari "aplikasi saya tidak mau berhenti": sebuah coroutine menangkap asyncio.CancelledError untuk keperluan pembersihan dan lupa untuk melemparkannya kembali. Task terus berjalan; pemanggil yang meminta pembatalan menunggu selamanya hingga task tersebut selesai. Selalu lempar ulang setelah pembersihan, atau gunakan blok try/finally alih-alih except eksplisit.

8.15.4. Mengubah state bersama di antara awaits

Penjadwalan kooperatif menjamin bahwa sebuah coroutine memiliki CPU untuk dirinya sendiri di antara awaits, tetapi begitu ia awaits, coroutine lain dapat berjalan. Jika dua coroutine memodifikasi struktur data yang sama dalam langkah-langkah yang menyertakan await, operasi mereka dapat saling silang dengan cara yang merusak struktur tersebut:

# bug: two tasks running do_work simultaneously can
# interleave around the await and corrupt items
async def do_work():
    n = len(items)
    await asyncio.sleep_ms(0)
    items.append(some_work(n))

Untuk state yang hanya diubah di antara awaits di dalam satu coroutine, tidak diperlukan sinkronisasi. Untuk state yang diubah di antara awaits dan diakses dari lebih dari satu coroutine, bungkus bagian kritis dalam Lock.

8.15.5. await di tingkat modul

await hanya valid di dalam isi async def. Menulisnya di tingkat modul -- di luar coroutine mana pun -- adalah kesalahan sintaks:

# bug: not inside an async def
result = await fetch()

Solusinya adalah menempatkan pekerjaan dalam sebuah coroutine dan memanggilnya dari titik masuk asyncio.run() program.

8.15.6. Beberapa pemanggilan asyncio.run

MicroPython memiliki satu event loop. Memanggil asyncio.run() dua kali berturut-turut -- sekali untuk setup, sekali untuk pekerjaan utama -- tetap menggunakan loop yang sama. Memanggilnya dari dalam coroutine yang sedang berjalan adalah kesalahan: loop sudah berjalan. Kedua kasus ini paling sering muncul ketika sebuah skrip berkembang secara organik dan penulis mencoba untuk memperluasnya dengan menambahkan lebih banyak pemanggilan run() alih-alih memasukkan pekerjaan baru ke dalam main yang sudah ada.

8.15.7. Penggunaan Event dari interupsi

asyncio.Event.set() hanya aman dipanggil dari dalam event loop. Memanggilnya dari handler interupsi GPIO adalah risiko korupsi data. Untuk membangunkan sebuah task dari interupsi, gunakan ThreadSafeFlag sebagai gantinya -- halamannya membahas polanya.

8.15.8. Pemanggilan sinkronus yang panjang

Sebuah coroutine dapat mengawait primitif penantian asyncio sendiri; hal lain yang dipanggilnya berjalan secara sinkronus dan memblokir loop hingga ia selesai. time.sleep() blokir 200 ms, penulisan kartu SD yang membutuhkan 80 ms untuk flush, kompresi JPEG besar, pemanggilan csi.CSI.snapshot() -- masing-masing menahan event loop selama durasi penuhnya. Solusinya tergantung pada pemanggilan:

  • Untuk time.sleep: ganti dengan await asyncio.sleep atau await asyncio.sleep_ms.

  • Untuk csi.CSI.snapshot: gunakan pembungkus snapshot async yang dibangun oleh halaman capture.

  • Untuk komputasi panjang (pemrosesan citra, encoding JPEG): terima biayanya atau pecah pekerjaan menjadi potongan-potongan yang await di antara iterasi.

Asyncio tidak dapat membuat pemanggilan sinkronus menjadi non-blokir. Ia hanya dapat membiarkan coroutine lain berjalan saat sesuatu yang lain sedang menunggu.