Cara Kerja Python asyncio: Membangunnya Ulang dari Awal
(jacobpadilla.com)- asyncio di Python adalah alat untuk menangani program I/O-bound, dan artikel ini menunjukkan cara kerjanya dengan membuat ulang implementasi sederhana di atas generator
- Intinya adalah struktur di mana event loop yang memiliki beberapa task menjalankan task, menerima kembali kontrol pada
yield, lalu berpindah ke task berikutnya sleepdapat dibuat sebagai sub-generator yang terus melakukanyieldsampai waktu berlalu, danyield frommembuat task semula berlanjut setelah generator bawahannya selesai- Jika diubah ke
async/await,Task.__await__()menyerahkan kontrol ke event loop sampai selesai, sementaracreate_taskdanrunmembentuk API yang mirip dengan asyncio sebenarnya - Jika pemanggilan
jacobiokustom diganti denganasyncio, kodenya menjadi kode yang memakai paket sebenarnya, tetapi asyncio nyata jauh lebih kompleks daripada contoh dan sebagian alur internalnya juga berbeda
Struktur dasar asyncio dilihat dari generator
- asyncio digunakan di Python untuk menangani program I/O-bound, dan artikel ini merekonstruksinya secara sederhana berbasis generator
- Seperti
rangedi Python 3+, generator menghasilkan nilai yang dibutuhkan satu per satu tanpa menyimpan seluruh sekuens di memori- Jika
range(100_000_000)dibuat seperti list, harus menyimpan 100 juta elemen sehingga sangat tidak efisien secara memori - Generator tidak menyimpan seluruh sekuens karena nilainya dibuat setiap kali dibutuhkan
- Jika
- Fungsi generator didefinisikan seperti fungsi biasa, tetapi menggunakan
yield- Saat fungsi dipanggil, body tidak langsung dieksekusi, melainkan mengembalikan objek generator
- Saat
next(iterator)dipanggil, eksekusi berjalan sampaiyieldberikutnya - Jika tidak ada lagi
yield, exceptionStopIterationterjadi
yield frommemungkinkan generator memanggil sub-generator atau objek iterable, sehingga dapat membentuk rantai generator- Karakteristik penting di sini adalah eksekusi fungsi dapat dihentikan sementara lalu dimulai kembali sambil mempertahankan state-nya
Menyederhanakan event loop sebagai list
- Event loop adalah inti asyncio yang menjalankan dan mengelola task saat ini
- Event loop asyncio sebenarnya ditulis dalam C, tetapi dalam model sederhana dapat dilihat sebagai list yang berisi task saat ini
- Pada contoh awal, task diperlakukan sebagai objek generator
- Pengelola event loop menelusuri list task
- Ia memanggil
next(task)pada setiap task untuk menjalankannya - Task berhenti dengan
yieldpada saat harus menunggu seperti operasi I/O-bound, lalu mengembalikan kontrol ke event loop
- Dalam contoh di mana
task1()dantask2()masing-masing mencetak lalu melakukanyield, output muncul bergantian- Karena kedua fungsi memiliki loop
while True, eksekusi terus berlanjut - Output berulang seperti
Task 1,Task 2,Task 1,Task 2
- Karena kedua fungsi memiliki loop
Membuat sleep dengan yield from
sleep(seconds)diimplementasikan sebagai generator yang mencatat waktu mulai lalu terus melakukanyieldsampai waktu yang ditentukan berlalu- Fungsi task mendelegasikan eksekusi ke sub-generator sleep seperti
yield from sleep(1)atauyield from sleep(5)- Selama
sleepterus melakukanyield, task berada dalam keadaan berhenti sementara - Jika waktu yang cukup telah berlalu, loop
whilepadasleepselesai - Karena tidak ada lagi
yield,StopIterationterjadi, danyield frommelanjutkan ke baris berikutnya pada fungsi task
- Selama
- Dalam contoh,
task1mencetak setiap 1 detik dantask2mencetak setiap 5 detik- Output menjadi bentuk di mana setelah
Task 1,Task 2,Task 1muncul beberapa kali laluTask 2muncul lagi
- Output menjadi bentuk di mana setelah
Beralih dari yield ke await
- Untuk menggunakan
await, objek target harus memiliki metode__await__atau berupa coroutine - Dalam asyncio, biasanya objek
Taskditangani lewat fungsi sepertiasyncio.create_task- Objek
Taskmewarisi objekFuturemilik asyncio - Objek
Futurememiliki metode__await__
- Objek
- Jika fungsi dengan keyword
asyncdipanggil, objek coroutine dibuat- Coroutine dapat menghentikan eksekusi sementara lalu melanjutkannya lagi seperti fungsi generator
awaitdapat dilihat mirip denganyield fromyang memiliki aturan validasi tambahanawait objectmenjadi bentuk yang melakukanyielddari__await__milik instance objek, atau menunggu coroutine lain
- Di kode sumber
Futuremilik asyncio juga dapat dilihat bahwa saatFutureatauTaskbelum selesai,__await__pada dasarnya memanggilyield
Implementasi sederhana Task, create_task, dan run
- Implementasi kustom menggunakan
Queuesebagai event loop, bukan list- Strukturnya bertujuan menangani operasi menambah dan menghapus task dari loop dalam waktu konstan
- Class
Taskmenyimpan objek generator dan status selesai- Objek generator disimpan di
self.iter self.finisheddimulai sebagaiFalse- Jika generator memunculkan
StopIteration, ia dianggap selesai done()mengembalikan status selesai
- Objek generator disimpan di
Task.__await__()terus memanggilyield selfselama task belum selesai- Perilaku ini mengembalikan kontrol ke event loop
create_task(generator)membungkus generator menjadiTask, memasukkannya ke antrean event loop, lalu mengembalikannya- Perannya adalah menjadwalkan task pada event loop
run(main)memulai event loop dengan bentuk yang miripasyncio.run()sebenarnyamainyang pertama diterima dibungkus menjadiTaskdan dimasukkan ke antrean- Selama antrean tidak kosong, task berikutnya diambil
- Task dijalankan dengan
task.iter.send(None) - Jika
StopIterationterjadi,task.finished = Truedisetel - Jika tidak ada exception, task dimasukkan kembali ke antrean event loop
- Penggunaan
task.iter.send(None)alih-alihnext(task.iter)adalah karakteristik saat bekerja bersama keywordasync/await, dan di sini perannya sama
Sleep yang kompatibel dengan async dan contoh jacobio
sleepsebelumnya adalah fungsi generator, tetapiawaittidak dapat digabungkan langsung dengan fungsi generator- Target
awaitharus berupa objek yang memiliki__await__atau fungsi coroutine
- Target
- Logika menunggu yang sebenarnya dipindahkan ke generator
_sleep(seconds)_sleepmelakukanyieldsampai waktu tertentu berlalu
async def sleep(seconds)membuat task dengan_sleep(seconds)lalu melakukanawaitpada task ituawait taskmemanggilTask.__await__()- Jika task belum selesai, kontrol diserahkan ke event loop lewat
yield
- File kustom yang selesai,
jacobio.py, mencakup elemen berikut- Antrean event loop
_sleepasync sleepTaskcreate_taskrun
- Pada contoh penggunaan,
yield fromlama diganti denganawait, dan fungsi yang memakaiawaitdiberiasynctask1mencetak dua kali dan setiap kali menunggujacobio.sleep(1)task2mencetak tiga kali dan setiap kali menunggujacobio.sleep(0)mainmembuat dua task, menunggu keduanya denganawait, lalu mencetakdone
- Output contoh berurutan
Task 1,Task 2,Task 2,Task 2,Task 1,done
Mengubahnya ke asyncio sebenarnya
- Jika pada contoh
jacobiokustom semuajacobiodiganti menjadiasyncio, kodenya menjadi kode yang menggunakan paket asyncio sebenarnya - Fungsi yang berpadanan adalah sebagai berikut
jacobio.sleep()→asyncio.sleep()jacobio.create_task()→asyncio.create_task()jacobio.run()→asyncio.run()
- asyncio sebenarnya melakukan jauh lebih banyak hal di dalamnya
- Pengelola event loop ini adalah implementasi yang dibuat sesederhana mungkin, sehingga menunjukkan ide dasar asyncio, tetapi karena skala dan kompleksitas paket sebenarnya, alurnya sedikit berbeda dari kode sumber nyata
- Jika menggunakan asyncio sebenarnya, alih-alih membuat dua task masing-masing dan menunggu keduanya dengan
await, beberapa task dapat ditangani dengan fungsi sepertiasyncio.gather() - Artikel terkait yang ditautkan adalah handling asyncio tasks like a pro
1 komentar
Komentar Hacker News
Asyncio bisa mengganti event loop dengan implementasi yang dibuat sendiri
Di Temporal Python, workflow direpresentasikan dengan event loop asyncio yang tahan lama kustom, sehingga hal seperti
asyncio.sleeppun menjadi timer yang tahan lama. Artinya kode bisa dilanjutkan di mesin lain, sehingga sleep selama beberapa minggu pun dimungkinkanCara implementasinya dijelaskan di artikel ini: https://temporal.io/blog/durable-distributed-asyncio-event-l...
Masalah terbesar asyncio adalah di Python, panggilan sinkron bisa memblokir thread asyncio dan dengan mudah membuat seluruh sistem berhenti. Python sangat membutuhkan alat analisis statis yang membuat call graph untuk mendeteksi apakah panggilan yang memblokir thread dipanggil secara langsung atau tidak langsung di dalam
async defOperasi dasar seperti aritmetika atau akses struktur data bisa dimasukkan ke whitelist sebagai panggilan sinkron yang cepat, lalu jika event loop berputar dengan kecepatan yang mencurigakan, mungkin pekerjaan lain yang seharusnya sinkron juga bisa dideteksi
Saat mengetahui bagaimana mereka mengimplementasikan event loop asyncio, itu benar-benar menjadi momen pencerahan bagi saya
Saya masih mencoba memahami apakah Python memang bahasa yang cocok untuk concurrency. Asyncio selalu terasa seperti bertahan seadanya, dan menurut saya implementasi async di C# lebih rapi
Purchaser.purchaseseharusnyado_purchase?Bahkan
read()sederhana pun bisa memblokir atau tidak, tergantung apa descriptornya dan bagaimana pengaturannya. Bagaimana mendeteksi ini dengan analisis statis?Implementasi ini tampaknya melakukan busy waiting saat sleep. Artinya event loop terus berputar meskipun tidak ada task yang bisa dijalankan saat ini
Saya ingat pernah melihat implementasi mainan lain dulu, yang melacak waktu task berikutnya bisa dijalankan dalam urutan tersortir, lalu menidurkan event loop itu sendiri jika tidak ada task yang bisa dijalankan saat ini. Sejauh pemahaman saya, asyncio sungguhan juga bekerja seperti ini
Setelah itu, ini bisa diperluas agar kondisi berikutnya untuk bisa dijalankan tidak hanya berupa waktu jam dinding sederhana, tetapi juga bisa memiliki dependensi pada sesuatu seperti socket, sehingga
selectbisa digunakan dengan timeoutSalah satu implementasi terkenal adalah uvloop(https://github.com/MagicStack/uvloop), yang pada dasarnya menggunakan libuv untuk mengimplementasikan loop, dan libuv menangani pekerjaan seperti
selectyang dijelaskan tadi[1] https://simpy.readthedocs.io/en/latest/
[2] https://gitlab.com/team-simpy/simpy.io
Event loop tidak harus selalu melakukan loop; ia bisa memulai dengan menjalankan
main, lalu selesai bersamanya ketika itu selesai. Bayangkan memulai server yang memiliki loopwhile trueyang menunggu socket, lalu berhenti pada kondisi terminasi atau interrupt sehingga program juga berakhirDari sudut pandang event loop, tidak ada busy waiting dan tidak perlu menyentuh sleep atau socket. Ini perbedaan antara menjalankan sampai selesai dan menjalankan selamanya
Kalau membuat loop mainan, menurut saya kasus berjalan selamanya sebaiknya tidak perlu ditangani
Ceramah David Beazley tentang asyncio sangat bagus
Berdasarkan itu, saya membuat alat simulasi kejadian diskret. Cukup keren bahwa kita bisa mengimplementasikan asyncio sendiri dan mengganti jam sistem dengan waktu simulasi
Artikel yang benar-benar bagus, dan merupakan penjelasan tingkat tinggi yang berhasil melewati bagian-bagian yang bisa membuat pembaca pertama kali bosan
Akan lebih baik kalau di bagian akhir ditambahkan materi lain yang menjelaskan bagaimana sebenarnya cara kerjanya di dalam
Saya rasa akan lebih baik kalau artikelnya menunjukkan hal yang sama tanpa memakai
yieldsama sekali. Jujur, bagian yang benar-benar terasa seperti sihir ada di situPenjelasan yang jauh lebih mendalam tentang coroutine Python ada di sini: https://aosabook.org/en/500L/a-web-crawler-with-asyncio-coro...
Tidak ada penyebutan
poll()sama sekali? Kalau begitu, ini sama sekali berbeda dari cara kerja asyncioBenar-benar aneh bahwa Python membajak
defdan memakainya untuk membuat objek yang sebenarnya bukan fungsiSetidaknya mereka bisa saja membuat keyword lain
Bentuknya mengembalikan generator atau coroutine, dan signature tipenya bisa dilihat di sini [1]. Bahkan tanpa syntax sugar di level bahasa, hal serupa bisa dilakukan di Python murni dengan sesuatu seperti decorator
[1] https://docs.python.org/3/library/typing.html#typing.Generat...
Namun, signature tipe itu saya sebutkan untuk kepentingan akademis; dalam anotasi tipe nyata, saya lebih memilih
IterabledanAwaitableyang lebih sederhanaTerdengar seperti mereka menempatkan semantik pengalaman developer di atas hal seperti kemurnian bahasa. Saya tidak pernah bingung bahwa fungsi
asyncadalah fungsi asinkrondef, sehingga tidak bisa membuat fungsi inline seperti arrow function JS atau lambda C++Ini bukan masalah kemurnian bahasa, melainkan memang tidak praktis