3 poin oleh GN⁺ 2024-05-09 | 1 komentar | Bagikan ke WhatsApp
  • 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
  • sleep dapat dibuat sebagai sub-generator yang terus melakukan yield sampai waktu berlalu, dan yield from membuat task semula berlanjut setelah generator bawahannya selesai
  • Jika diubah ke async/await, Task.__await__() menyerahkan kontrol ke event loop sampai selesai, sementara create_task dan run membentuk API yang mirip dengan asyncio sebenarnya
  • Jika pemanggilan jacobio kustom diganti dengan asyncio, 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 range di 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
  • 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 sampai yield berikutnya
    • Jika tidak ada lagi yield, exception StopIteration terjadi
  • yield from memungkinkan 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 yield pada saat harus menunggu seperti operasi I/O-bound, lalu mengembalikan kontrol ke event loop
  • Dalam contoh di mana task1() dan task2() masing-masing mencetak lalu melakukan yield, output muncul bergantian
    • Karena kedua fungsi memiliki loop while True, eksekusi terus berlanjut
    • Output berulang seperti Task 1, Task 2, Task 1, Task 2

Membuat sleep dengan yield from

  • sleep(seconds) diimplementasikan sebagai generator yang mencatat waktu mulai lalu terus melakukan yield sampai waktu yang ditentukan berlalu
  • Fungsi task mendelegasikan eksekusi ke sub-generator sleep seperti yield from sleep(1) atau yield from sleep(5)
    • Selama sleep terus melakukan yield, task berada dalam keadaan berhenti sementara
    • Jika waktu yang cukup telah berlalu, loop while pada sleep selesai
    • Karena tidak ada lagi yield, StopIteration terjadi, dan yield from melanjutkan ke baris berikutnya pada fungsi task
  • Dalam contoh, task1 mencetak setiap 1 detik dan task2 mencetak setiap 5 detik
    • Output menjadi bentuk di mana setelah Task 1, Task 2, Task 1 muncul beberapa kali lalu Task 2 muncul lagi

Beralih dari yield ke await

  • Untuk menggunakan await, objek target harus memiliki metode __await__ atau berupa coroutine
  • Dalam asyncio, biasanya objek Task ditangani lewat fungsi seperti asyncio.create_task
    • Objek Task mewarisi objek Future milik asyncio
    • Objek Future memiliki metode __await__
  • Jika fungsi dengan keyword async dipanggil, objek coroutine dibuat
    • Coroutine dapat menghentikan eksekusi sementara lalu melanjutkannya lagi seperti fungsi generator
  • await dapat dilihat mirip dengan yield from yang memiliki aturan validasi tambahan
    • await object menjadi bentuk yang melakukan yield dari __await__ milik instance objek, atau menunggu coroutine lain
  • Di kode sumber Future milik asyncio juga dapat dilihat bahwa saat Future atau Task belum selesai, __await__ pada dasarnya memanggil yield

Implementasi sederhana Task, create_task, dan run

  • Implementasi kustom menggunakan Queue sebagai event loop, bukan list
    • Strukturnya bertujuan menangani operasi menambah dan menghapus task dari loop dalam waktu konstan
  • Class Task menyimpan objek generator dan status selesai
    • Objek generator disimpan di self.iter
    • self.finished dimulai sebagai False
    • Jika generator memunculkan StopIteration, ia dianggap selesai
    • done() mengembalikan status selesai
  • Task.__await__() terus memanggil yield self selama task belum selesai
    • Perilaku ini mengembalikan kontrol ke event loop
  • create_task(generator) membungkus generator menjadi Task, memasukkannya ke antrean event loop, lalu mengembalikannya
    • Perannya adalah menjadwalkan task pada event loop
  • run(main) memulai event loop dengan bentuk yang mirip asyncio.run() sebenarnya
    • main yang pertama diterima dibungkus menjadi Task dan dimasukkan ke antrean
    • Selama antrean tidak kosong, task berikutnya diambil
    • Task dijalankan dengan task.iter.send(None)
    • Jika StopIteration terjadi, task.finished = True disetel
    • Jika tidak ada exception, task dimasukkan kembali ke antrean event loop
  • Penggunaan task.iter.send(None) alih-alih next(task.iter) adalah karakteristik saat bekerja bersama keyword async/await, dan di sini perannya sama

Sleep yang kompatibel dengan async dan contoh jacobio

  • sleep sebelumnya adalah fungsi generator, tetapi await tidak dapat digabungkan langsung dengan fungsi generator
    • Target await harus berupa objek yang memiliki __await__ atau fungsi coroutine
  • Logika menunggu yang sebenarnya dipindahkan ke generator _sleep(seconds)
    • _sleep melakukan yield sampai waktu tertentu berlalu
  • async def sleep(seconds) membuat task dengan _sleep(seconds) lalu melakukan await pada task itu
    • await task memanggil Task.__await__()
    • Jika task belum selesai, kontrol diserahkan ke event loop lewat yield
  • File kustom yang selesai, jacobio.py, mencakup elemen berikut
    • Antrean event loop
    • _sleep
    • async sleep
    • Task
    • create_task
    • run
  • Pada contoh penggunaan, yield from lama diganti dengan await, dan fungsi yang memakai await diberi async
    • task1 mencetak dua kali dan setiap kali menunggu jacobio.sleep(1)
    • task2 mencetak tiga kali dan setiap kali menunggu jacobio.sleep(0)
    • main membuat dua task, menunggu keduanya dengan await, lalu mencetak done
  • Output contoh berurutan Task 1, Task 2, Task 2, Task 2, Task 1, done

Mengubahnya ke asyncio sebenarnya

  • Jika pada contoh jacobio kustom semua jacobio diganti menjadi asyncio, 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 seperti asyncio.gather()
  • Artikel terkait yang ditautkan adalah handling asyncio tasks like a pro

1 komentar

 
GN⁺ 2024-05-09
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.sleep pun menjadi timer yang tahan lama. Artinya kode bisa dilanjutkan di mesin lain, sehingga sleep selama beberapa minggu pun dimungkinkan
    Cara 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 def

    • Mungkin ini ide yang buruk, tetapi saya terpikir bagaimana kalau dibalik, alih-alih semua panggilan secara default bersifat sinkron seperti sekarang
      Operasi 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
    • Temporal benar-benar keren
      Saat mengetahui bagaimana mereka mengimplementasikan event loop asyncio, itu benar-benar menjadi momen pencerahan bagi saya
    • Saya penasaran kenapa tidak memakai thread
      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
    • Dalam contoh workflow, bukankah Purchaser.purchase seharusnya do_purchase?
    • Menangkap hal itu secara statis tidak akan mudah
      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 select bisa digunakan dengan timeout

    • Di asyncio sendiri pun, selain implementasi bawaan, kita bisa memakai implementasi event loop kustom
      Salah satu implementasi terkenal adalah uvloop(https://github.com/MagicStack/uvloop), yang pada dasarnya menggunakan libuv untuk mengimplementasikan loop, dan libuv menangani pekerjaan seperti select yang dijelaskan tadi
    • Ini terdengar cukup mirip dengan SimPy [1] / Simpy.io [2], tetapi SimPy muncul beberapa tahun lebih dulu daripada asyncio
      [1] https://simpy.readthedocs.io/en/latest/
      [2] https://gitlab.com/team-simpy/simpy.io
    • Dari satu sudut pandang, tidak ada masalah khusus dengan pendekatan ini
      Event loop tidak harus selalu melakukan loop; ia bisa memulai dengan menjalankan main, lalu selesai bersamanya ketika itu selesai. Bayangkan memulai server yang memiliki loop while true yang menunggu socket, lalu berhenti pada kondisi terminasi atau interrupt sehingga program juga berakhir
      Dari 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
    • Buku terbaik untuk mempelajari hal seperti ini apa?
  • 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 yield sama sekali. Jujur, bagian yang benar-benar terasa seperti sihir ada di situ
    Penjelasan 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 asyncio

  • Benar-benar aneh bahwa Python membajak def dan memakainya untuk membuat objek yang sebenarnya bukan fungsi
    Setidaknya mereka bisa saja membuat keyword lain

    • Itu memang fungsi, hanya saja tipe kembaliannya diubah menjadi fungsi 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 Iterable dan Awaitable yang lebih sederhana
    • Masalah sebenarnya apa? Atau ini sekadar soal kemurnian bahasa?
    • Ini bukan sesuatu yang perlu dianggap tragedi
      Terdengar seperti mereka menempatkan semantik pengalaman developer di atas hal seperti kemurnian bahasa. Saya tidak pernah bingung bahwa fungsi async adalah fungsi asinkron
    • Masalah Python yang lebih besar adalah sejak awal fungsi harus memakai def, sehingga tidak bisa membuat fungsi inline seperti arrow function JS atau lambda C++
      Ini bukan masalah kemurnian bahasa, melainkan memang tidak praktis
    • Apakah yang dimaksud decorator? Dalam konteks Python, cukup membingungkan untuk memahami apa yang sedang dibicarakan