8.5. Timeout dan pembatalan¶
Pembatalan adalah istilah asyncio untuk "hentikan eksekusi coroutine ini, munculkan pengecualian di dalamnya agar dapat membersihkan diri, dan hapus dari penjadwal". Timeout adalah alasan paling umum untuk membatalkan sesuatu; task.cancel() secara manual adalah alasan lainnya.
8.5.1. Membatalkan sebuah task¶
Memanggil Task.cancel menjadwalkan asyncio.CancelledError untuk dimunculkan di dalam coroutine yang sedang berjalan pada await berikutnya. Coroutine bebas untuk mengabaikan pembatalan (menangkap pengecualian dan terus berjalan) atau mematuhinya -- pilihan biasanya adalah mematuhinya setelah menjalankan kode pembersihan:
async def network_worker(stream):
try:
while True:
data = await stream.read(64)
# ...
finally:
stream.close()
Klausa finally polos adalah pola paling bersih: pembersihan berjalan baik saat coroutine keluar secara normal, memunculkan pengecualian yang tidak terkait, maupun dibatalkan. CancelledError merambat kembali melalui finally, loop melihat task telah selesai, dan pemanggil await task melihat pembatalan.
Menangkap CancelledError secara eksplisit juga tidak masalah ketika aplikasi ingin melakukan sesuatu yang spesifik dengannya -- mencatatnya, menyerahkan sumber daya dengan bersih, dll. Aturannya adalah: munculkan kembali setelah pembersihan selesai. Menelan CancelledError membuat coroutine tetap hidup ketika pemanggilnya telah memintanya untuk berhenti, yang hampir selalu merupakan bug:
async def worker():
try:
await long_running_thing()
except asyncio.CancelledError:
log("worker cancelled, cleaning up")
close_resources()
raise # << this line is the important one
8.5.2. Timeout dengan wait_for¶
asyncio.wait_for() membungkus sebuah awaitable dengan batas waktu. Jika awaitable selesai dalam batas waktu, hasilnya dikembalikan. Jika tidak, awaitable dibatalkan dan pemanggil mendapatkan asyncio.TimeoutError
try:
frame = await asyncio.wait_for(grab_frame(), timeout=2)
except asyncio.TimeoutError:
print("camera took too long")
Argumen detik menerima float. Untuk batas waktu dalam milidetik, asyncio.wait_for_ms() menerima hitungan integer milidetik -- ekstensi MicroPython yang selaras dengan knop pengaturan waktu berbasis milidetik pada firmware.
Secara internal, wait_for melakukan persis apa yang dilakukan pembatalan manual: ketika batas waktu berakhir, ia memanggil cancel() pada task yang dibungkus, CancelledError dimunculkan di dalam coroutine, klausa finally dijalankan, dan setelah pembersihan selesai pengecualian diterjemahkan menjadi TimeoutError untuk pemanggil.
Artinya, coroutine yang menangkap dan mengabaikan CancelledError akan mengalahkan timeout -- batas waktu telah berakhir, tetapi coroutine menolak untuk berhenti, dan wait_for tidak dapat memaksanya. Aturan sebelumnya berlaku di sini juga: tangkap CancelledError hanya untuk menjalankan pembersihan, lalu munculkan kembali.
8.5.3. Pembatalan melalui gather¶
Pembatalan merambat ke bawah melalui gather(). Jika task yang menunggu panggilan gather dibatalkan, setiap awaitable yang masih berjalan di dalam gather juga dibatalkan -- masing-masing mendapat kesempatan untuk membersihkan diri melalui klausa finally-nya sendiri sebelum pembatalan diteruskan ke pemanggil.
Dikombinasikan dengan timeout, ini adalah cara standar untuk memberikan batas waktu pada sekelompok operasi:
await asyncio.wait_for(
asyncio.gather(a(), b(), c()),
timeout=5,
)
Setiap sub-operasi harus selesai dalam lima detik, atau semua akan dibatalkan bersama.
8.5.4. Menghentikan aplikasi¶
Pembatalan juga merupakan cara sebuah aplikasi nyata berhenti dengan bersih. Pola ini konsisten di semua skrip: main menangkap handle untuk task latar belakang yang berjalan lama yang telah dimulainya, menjalankan pekerjaan tingkat atasnya, lalu membatalkan setiap handle dan menunggunya dalam blok finally
async def main():
sender = asyncio.create_task(uplink())
watcher = asyncio.create_task(button_watcher())
try:
await snapshot_loop()
finally:
sender.cancel()
watcher.cancel()
await asyncio.gather(sender, watcher,
return_exceptions=True)
return_exceptions=True adalah trik yang mencegah gather memunculkan kembali CancelledError yang akan disampaikan oleh setiap task anak, sehingga alasan keluar aplikasi sendiri -- apa pun yang snapshot_loop lakukan atau tidak lakukan -- adalah yang muncul dari main.