Pertanyaan ini dimotivasi oleh pertanyaan saya yang lain: Bagaimana cara menunggu di cdef?
Ada banyak sekali artikel dan postingan blog di web tentang asyncio
, tetapi semuanya sangat dangkal. Saya tidak dapat menemukan informasi apa pun tentang bagaimana asyncio
sebenarnya diterapkan, dan apa yang membuat I / O asinkron. Saya mencoba membaca kode sumber, tetapi itu ribuan baris bukan kode C kelas tertinggi, banyak di antaranya berkaitan dengan objek tambahan, tetapi yang paling penting, sulit untuk menghubungkan antara sintaks Python dan kode C apa yang akan diterjemahkan ke.
Dokumentasi Asycnio sendiri bahkan kurang membantu. Tidak ada informasi di sana tentang cara kerjanya, hanya beberapa pedoman tentang cara menggunakannya, yang terkadang juga menyesatkan / ditulis dengan sangat buruk.
Saya akrab dengan implementasi coroutine Go, dan agak berharap Python melakukan hal yang sama. Jika itu masalahnya, kode yang saya dapatkan di pos yang ditautkan di atas akan berfungsi. Karena tidak, saya sekarang mencoba mencari tahu mengapa. Tebakan terbaik saya sejauh ini adalah sebagai berikut, tolong perbaiki jika saya salah:
- Definisi prosedur bentuk
async def foo(): ...
sebenarnya diartikan sebagai metode mewarisi kelascoroutine
. - Mungkin,
async def
sebenarnya dipecah menjadi beberapa metode berdasarkanawait
pernyataan, di mana objek, tempat metode ini dipanggil, dapat melacak kemajuan yang dibuatnya melalui eksekusi sejauh ini. - Jika hal di atas benar, maka, pada dasarnya, eksekusi coroutine bermuara pada metode pemanggilan objek coroutine oleh beberapa manajer global (loop?).
- Manajer global entah bagaimana (bagaimana?) Mengetahui kapan operasi I / O dilakukan oleh kode Python (hanya?) Dan dapat memilih salah satu metode coroutine yang tertunda untuk dieksekusi setelah metode eksekusi saat ini melepaskan kontrol (klik pada
await
pernyataan ).
Dengan kata lain, inilah upaya saya untuk "mendesain" beberapa asyncio
sintaks menjadi sesuatu yang lebih dapat dimengerti:
async def coro(name):
print('before', name)
await asyncio.sleep()
print('after', name)
asyncio.gather(coro('first'), coro('second'))
# translated from async def coro(name)
class Coro(coroutine):
def before(self, name):
print('before', name)
def after(self, name):
print('after', name)
def __init__(self, name):
self.name = name
self.parts = self.before, self.after
self.pos = 0
def __call__():
self.parts[self.pos](self.name)
self.pos += 1
def done(self):
return self.pos == len(self.parts)
# translated from asyncio.gather()
class AsyncIOManager:
def gather(*coros):
while not every(c.done() for c in coros):
coro = random.choice(coros)
coro()
Jika tebakan saya terbukti benar: maka saya punya masalah. Bagaimana sebenarnya I / O terjadi dalam skenario ini? Di utas terpisah? Apakah seluruh juru bahasa ditangguhkan dan I / O terjadi di luar penerjemah? Apa sebenarnya yang dimaksud dengan I / O? Jika prosedur python saya disebut prosedur C open()
, dan pada gilirannya mengirim interupsi ke kernel, melepaskan kontrol padanya, bagaimana penerjemah Python tahu tentang ini dan dapat terus menjalankan beberapa kode lain, sementara kode kernel melakukan I / O yang sebenarnya dan sampai itu membangunkan prosedur Python yang mengirim interupsi aslinya? Bagaimana interpreter Python pada prinsipnya menyadari hal ini terjadi?
sumber
BaseEventLoop
diimplementasikan: github.com/python/cpython/blob/…_run_once
, yang sebenarnya satu-satunya fungsi yang berguna di seluruh modul ini dibuat "pribadi"? Implementasinya mengerikan, tapi itu bukan masalah. Mengapa satu-satunya fungsi yang ingin Anda panggil pada event loop ditandai sebagai "jangan panggil saya"?_run_once
sejak awal?_run_once
?asyncio
rumit dan ada kesalahannya, tetapi harap pertahankan agar diskusi tetap sopan. Jangan menjelek-jelekkan pengembang di balik kode yang Anda sendiri tidak mengerti.Jawaban:
Bagaimana cara kerja asyncio?
Sebelum menjawab pertanyaan ini kita perlu memahami beberapa istilah dasar, lewati ini jika Anda sudah mengetahuinya.
Generator
Generator adalah objek yang memungkinkan kita untuk menangguhkan eksekusi fungsi python. Generator yang dikurasi pengguna diimplementasikan menggunakan kata kunci
yield
. Dengan membuat fungsi normal yang berisiyield
kata kunci, kami mengubah fungsi itu menjadi generator:Seperti yang Anda lihat, memanggil
next()
generator menyebabkan interpreter memuat frame pengujian, dan mengembalikan nilaiyield
ed. Memanggilnext()
lagi, menyebabkan frame memuat lagi ke tumpukan interpreter, dan melanjutkan keyield
nilai lain.Pada saat ketiga
next()
dipanggil, generator kami selesai, danStopIteration
terlempar.Berkomunikasi dengan generator
Fitur generator yang kurang dikenal adalah kenyataan bahwa Anda dapat berkomunikasi dengan mereka menggunakan dua metode:
send()
danthrow()
.Setelah memanggil
gen.send()
, nilai tersebut diteruskan sebagai nilai kembali dariyield
kata kunci.gen.throw()
di sisi lain, memungkinkan untuk melempar Pengecualian ke dalam generator, dengan pengecualian yang dimunculkan di tempat yang samayield
dipanggil.Mengembalikan nilai dari generator
Mengembalikan nilai dari generator, menghasilkan nilai yang dimasukkan ke dalam
StopIteration
pengecualian. Nanti kita dapat memulihkan nilai dari pengecualian dan menggunakannya untuk kebutuhan kita.Lihatlah, kata kunci baru:
yield from
Python 3.4 datang dengan penambahan kata kunci baru:
yield from
. Apa yang memungkinkan kata kunci itu untuk kita lakukan, meneruskan apa sajanext()
,send()
danthrow()
menjadi generator bersarang paling dalam. Jika generator bagian dalam mengembalikan nilai, itu juga merupakan nilai pengembalianyield from
:Saya telah menulis artikel untuk menguraikan lebih lanjut tentang topik ini.
Menyatukan semuanya
Setelah memperkenalkan kata kunci baru
yield from
di Python 3.4, kami sekarang dapat membuat generator di dalam generator yang seperti terowongan, meneruskan data bolak-balik dari generator paling dalam ke paling luar. Ini telah melahirkan arti baru bagi generator - coroutine .Coroutine adalah fungsi yang dapat dihentikan dan dilanjutkan saat dijalankan. Di Python, mereka didefinisikan menggunakan
async def
kata kunci. Sama seperti generator, mereka juga menggunakan bentuk mereka sendiriyield from
yangawait
. Sebelumasync
danawait
diperkenalkan dengan Python 3.5, kami membuat coroutine dengan cara yang sama persis dengan generator yang dibuat (denganyield from
alih - alihawait
).Seperti setiap iterator atau generator yang mengimplementasikan
__iter__()
metode ini, implementasi coroutine__await__()
yang memungkinkan mereka untuk melanjutkan setiap kaliawait coro
dipanggil.Ada diagram urutan yang bagus di dalam dokumen Python yang harus Anda periksa.
Dalam asyncio, selain fungsi coroutine, kami memiliki 2 objek penting: tugas dan masa depan .
Futures
Futures adalah objek yang
__await__()
metodenya telah diterapkan, dan tugasnya adalah mempertahankan keadaan dan hasil tertentu. Negara bagian dapat menjadi salah satu dari berikut ini:fut.cancel()
fut.set_result()
atau dengan set pengecualian menggunakanfut.set_exception()
Hasilnya, seperti yang Anda tebak, bisa berupa objek Python, yang akan dikembalikan, atau pengecualian yang mungkin dimunculkan.
Lain yang penting fitur dari
future
benda-benda, adalah bahwa mereka mengandung metode yang disebutadd_done_callback()
. Metode ini memungkinkan fungsi dipanggil segera setelah tugas selesai - baik itu memunculkan pengecualian atau selesai.Tugas
Objek tugas adalah masa depan khusus, yang membungkus coroutine, dan berkomunikasi dengan coroutine paling dalam dan paling luar. Setiap kali coroutine
await
sa masa depan, masa depan diteruskan kembali ke tugas (seperti diyield from
), dan tugas menerimanya.Selanjutnya, tugas tersebut mengikat dirinya ke masa depan. Ia melakukannya dengan menelepon
add_done_callback()
masa depan. Mulai sekarang, jika masa depan akan dilakukan, baik dengan dibatalkan, melewati pengecualian, atau meneruskan objek Python sebagai hasilnya, callback tugas akan dipanggil, dan akan bangkit kembali.Asyncio
Pertanyaan terakhir yang harus kita jawab adalah - bagaimana IO diimplementasikan?
Jauh di dalam asyncio, kami memiliki loop acara. Perulangan tugas. Tugas loop acara adalah memanggil tugas setiap kali mereka siap dan mengoordinasikan semua upaya itu ke dalam satu mesin yang berfungsi.
Bagian IO dari event loop dibangun di atas satu fungsi penting yang dipanggil
select
. Select adalah fungsi pemblokiran, diimplementasikan oleh sistem operasi di bawahnya, yang memungkinkan menunggu di soket untuk data masuk atau keluar. Setelah data diterima, ia bangun, dan mengembalikan soket yang menerima data, atau soket yang siap untuk ditulis.Ketika Anda mencoba menerima atau mengirim data melalui soket melalui asyncio, yang sebenarnya terjadi di bawah ini adalah soket diperiksa terlebih dahulu jika ada data yang dapat segera dibaca atau dikirim. Jika
.send()
buffernya penuh, atau.recv()
buffernya kosong, soket didaftarkan keselect
fungsi (hanya dengan menambahkannya ke salah satu daftar,rlist
untukrecv
danwlist
untuksend
) dan fungsi yang sesuai yangawait
baru dibuatfuture
, terikat ke soket itu.Ketika semua tugas yang tersedia menunggu masa depan, event loop memanggil
select
dan menunggu. Ketika salah satu soket memiliki data yang masuk, atausend
buffernya terkuras, asyncio memeriksa objek masa depan yang terkait dengan soket itu, dan menyetelnya ke selesai.Sekarang semua keajaiban terjadi. Masa depan diatur untuk selesai, tugas yang ditambahkan sebelumnya dengan
add_done_callback()
bangkit kembali, dan memanggil.send()
coroutine yang melanjutkan coroutine paling dalam (karenaawait
rantai) dan Anda membaca data yang baru diterima dari buffer terdekat itu tumpah ke.Rantai metode lagi, jika
recv()
:select.select
menunggu.future.set_result()
disebut.add_done_callback()
sekarang sudah aktif..send()
coroutine yang masuk ke coroutine paling dalam dan membangunkannya.Singkatnya, asyncio menggunakan kapabilitas generator, yang memungkinkan menjeda dan melanjutkan fungsi. Ini menggunakan
yield from
kemampuan yang memungkinkan melewatkan data bolak-balik dari generator paling dalam ke paling luar. Ia menggunakan semua itu untuk menghentikan eksekusi fungsi sambil menunggu IO selesai (dengan menggunakanselect
fungsi OS ).Dan yang terbaik dari semuanya? Saat satu fungsi dijeda, fungsi lainnya mungkin berjalan dan menyatu dengan kain halus, yaitu asyncio.
sumber
yield from
kerja. Namun saya mencatat di atas bahwa itu dapat dilewati jika pembaca sudah mengetahuinya :-) Ada hal lain yang menurut Anda harus saya tambahkan?select
mungkin memenuhi syarat juga, karena itu adalah bagaimana non-blocking I / O sistem panggilan bekerja pada OS.asyncio
Konstruksi aktual dan loop peristiwa hanyalah kode tingkat aplikasi yang dibuat dari hal-hal ini.Berbicara tentang
async/await
danasyncio
bukanlah hal yang sama. Yang pertama adalah konstruksi dasar tingkat rendah (coroutine) sedangkan yang berikutnya adalah pustaka yang menggunakan konstruksi ini. Sebaliknya, tidak ada jawaban akhir tunggal.Berikut ini adalah gambaran umum tentang bagaimana
async/await
danasyncio
-seperti perpustakaan bekerja. Artinya, mungkin ada trik lain di atas (ada ...) tetapi trik itu tidak penting kecuali Anda membuatnya sendiri. Perbedaannya harus dapat diabaikan kecuali Anda sudah cukup tahu untuk tidak perlu mengajukan pertanyaan seperti itu.1. Coroutine versus subrutin dalam kulit kacang
Sama seperti subrutin (fungsi, prosedur, ...), coroutine (generator, ...) adalah abstraksi tumpukan panggilan dan penunjuk instruksi: ada setumpuk potongan kode yang sedang dieksekusi, dan masing-masing berada pada instruksi tertentu.
Perbedaan
def
versusasync def
hanya untuk kejelasan. Perbedaan sebenarnya adalahreturn
versusyield
. Dari sini,await
atauyield from
ambil perbedaan dari panggilan individu ke seluruh tumpukan.1.1. Subrutin
Sebuah subrutin mewakili tingkat tumpukan baru untuk menampung variabel lokal, dan satu traversal instruksi untuk mencapai tujuan. Pertimbangkan subrutin seperti ini:
Saat Anda menjalankannya, itu artinya
bar
danqux
return
, dorong nilainya ke stack pemanggilKhususnya, 4. berarti bahwa subrutin selalu dimulai pada keadaan yang sama. Segala sesuatu yang eksklusif untuk fungsi itu sendiri akan hilang setelah selesai. Suatu fungsi tidak dapat dilanjutkan, meskipun ada instruksi setelahnya
return
.1.2. Coroutine sebagai subrutin yang persisten
Coroutine seperti subrutin, tetapi dapat keluar tanpa merusak statusnya. Pertimbangkan coroutine seperti ini:
Saat Anda menjalankannya, itu artinya
bar
danqux
yield
, dorong nilainya ke stack pemanggil tetapi simpan stack dan penunjuk instruksiyield
, pulihkan tumpukan dan penunjuk instruksi dan dorong argumen kequx
return
, dorong nilainya ke stack pemanggilPerhatikan penambahan 2.1 dan 2.2 - coroutine dapat ditangguhkan dan dilanjutkan pada poin yang telah ditentukan. Ini mirip dengan bagaimana subrutin ditangguhkan selama pemanggilan subrutin lain. Perbedaannya adalah bahwa coroutine aktif tidak terikat secara ketat ke stack pemanggilnya. Sebaliknya, coroutine yang ditangguhkan adalah bagian dari tumpukan terpisah dan terisolasi.
Ini berarti coroutine yang ditangguhkan dapat disimpan atau dipindahkan dengan bebas di antara tumpukan. Setiap tumpukan panggilan yang memiliki akses ke coroutine dapat memutuskan untuk melanjutkannya.
1.3. Melintasi tumpukan panggilan
Sejauh ini, coroutine kami hanya menggunakan call stack
yield
. Sebuah subrutin bisa turun dan naik tumpukan panggilan denganreturn
dan()
. Untuk kelengkapan, coroutine juga membutuhkan mekanisme untuk menaikkan tumpukan panggilan. Pertimbangkan coroutine seperti ini:Ketika Anda menjalankannya, itu berarti masih mengalokasikan stack dan penunjuk instruksi seperti subrutin. Saat dihentikan, itu masih seperti menyimpan subrutin.
Namun,
yield from
lakukan keduanya . Ini menangguhkan tumpukan dan penunjuk instruksiwrap
dan berjalancofoo
. Perhatikan bahwawrap
tetap ditangguhkan sampaicofoo
selesai sepenuhnya. Setiap kalicofoo
menangguhkan atau sesuatu dikirim,cofoo
langsung terhubung ke stack panggilan.1.4. Coroutine sampai ke bawah
Seperti yang sudah mapan,
yield from
memungkinkan untuk menghubungkan dua cakupan di satu lingkup perantara lainnya. Ketika diterapkan secara rekursif, itu berarti bagian atas tumpukan dapat dihubungkan ke bagian bawah tumpukan.Perhatikan itu
root
dancoro_b
tidak tahu tentang satu sama lain. Ini membuat coroutine jauh lebih bersih daripada callback: coroutine masih dibangun di atas relasi 1: 1 seperti subrutin. Coroutine menangguhkan dan melanjutkan seluruh tumpukan eksekusi yang ada hingga titik panggilan biasa.Khususnya,
root
dapat memiliki jumlah coroutine yang berubah-ubah untuk dilanjutkan. Namun, itu tidak pernah bisa melanjutkan lebih dari satu pada waktu yang sama. Coroutine dari root yang sama bersifat bersamaan tetapi tidak paralel!1.5. Python
async
danawait
Penjelasannya sejauh ini secara eksplisit menggunakan kosakata
yield
danyield from
generator - fungsionalitas yang mendasarinya sama. Sintaks Python3.5 baruasync
danawait
ada terutama untuk kejelasan.The
async for
danasync with
pernyataan diperlukan karena Anda akan memutusyield from/await
rantai dengan telanjangfor
danwith
pernyataan.2. Anatomi lingkaran peristiwa sederhana
Dengan sendirinya, coroutine tidak memiliki konsep untuk menyerahkan kendali ke coroutine lain . Itu hanya dapat menghasilkan kontrol ke pemanggil di bagian bawah tumpukan coroutine. Penelepon ini kemudian dapat beralih ke coroutine lain dan menjalankannya.
Node akar dari beberapa coroutine ini biasanya merupakan loop peristiwa : saat ditangguhkan, coroutine menghasilkan peristiwa yang ingin dilanjutkan. Pada gilirannya, loop peristiwa mampu menunggu peristiwa ini terjadi secara efisien. Ini memungkinkannya untuk memutuskan coroutine mana yang akan dijalankan berikutnya, atau bagaimana menunggu sebelum melanjutkan.
Desain seperti itu menyiratkan bahwa ada satu set peristiwa yang telah ditentukan sebelumnya yang dipahami oleh loop. Beberapa coroutine
await
satu sama lain, sampai akhirnya ada eventawait
. Peristiwa ini dapat berkomunikasi secara langsung dengan perulangan peristiwa denganyield
kontrol.Kuncinya adalah penangguhan coroutine memungkinkan loop peristiwa dan peristiwa untuk berkomunikasi secara langsung. Tumpukan coroutine menengah tidak memerlukan pengetahuan apa pun tentang loop mana yang menjalankannya, atau cara kerja peristiwa.
2.1.1. Acara tepat waktu
Peristiwa paling sederhana untuk ditangani adalah mencapai suatu titik waktu. Ini adalah blok fundamental dari kode berulir juga: utas berulang kali
sleep
sampai kondisi benar. Namun,sleep
eksekusi blok biasa dengan sendirinya - kami ingin coroutine lain tidak diblokir. Sebagai gantinya, kami ingin memberi tahu event loop kapan harus melanjutkan tumpukan coroutine saat ini.2.1.2. Mendefinisikan Acara
Peristiwa hanyalah nilai yang dapat kita identifikasi - baik itu melalui enum, jenis atau identitas lainnya. Kita dapat mendefinisikan ini dengan kelas sederhana yang menyimpan waktu target kita. Selain menyimpan informasi acara, kami dapat mengizinkan ke
await
kelas secara langsung.Kelas ini hanya menyimpan acara - tidak mengatakan bagaimana sebenarnya menanganinya.
Satu-satunya fitur khusus adalah
__await__
- itulah yangawait
dicari kata kunci. Secara praktis, ini adalah iterator tetapi tidak tersedia untuk mesin iterasi biasa.2.2.1. Menunggu acara
Sekarang kita punya acara, bagaimana reaksi coroutine terhadapnya? Kita harus bisa mengungkapkan padanannya
sleep
denganawait
acara kita. Untuk lebih melihat apa yang sedang terjadi, kami menunggu dua kali untuk separuh waktu:Kita dapat langsung membuat instance dan menjalankan coroutine ini. Mirip dengan generator, menggunakan
coroutine.send
menjalankan coroutine sampaiyield
hasilnya.Ini memberi kita dua
AsyncSleep
peristiwa dan kemudianStopIteration
saat coroutine selesai. Perhatikan bahwa satu-satunya penundaan adalah daritime.sleep
dalam loop! Masing-masingAsyncSleep
hanya menyimpan offset dari waktu saat ini.2.2.2. Acara + Tidur
Pada titik ini, kami memiliki dua mekanisme terpisah yang kami miliki:
AsyncSleep
Peristiwa yang dapat dihasilkan dari dalam coroutinetime.sleep
yang dapat menunggu tanpa memengaruhi coroutineKhususnya, keduanya ortogonal: tidak satu pun yang memengaruhi atau memicu yang lain. Alhasil, kita bisa memikirkan strategi kita sendiri
sleep
untuk mengatasi keterlambatan sebuahAsyncSleep
.2.3. Perulangan peristiwa yang naif
Jika kami memiliki beberapa coroutine, masing-masing dapat memberi tahu kami kapan ingin dibangunkan. Kemudian kita bisa menunggu sampai yang pertama ingin dilanjutkan, lalu yang setelahnya, dan seterusnya. Khususnya, di setiap titik kami hanya peduli tentang mana yang berikutnya .
Ini membuat penjadwalan menjadi mudah:
Implementasi sepele tidak membutuhkan konsep lanjutan. A
list
memungkinkan untuk mengurutkan coroutine berdasarkan tanggal. Menunggu adalah hal yang biasatime.sleep
. Menjalankan coroutine berfungsi seperti sebelumnya dengancoroutine.send
.Tentu saja, ini memiliki banyak ruang untuk perbaikan. Kita bisa menggunakan heap untuk antrian tunggu atau tabel pengiriman untuk acara. Kita juga bisa mengambil nilai kembali dari
StopIteration
dan menetapkannya ke coroutine. Namun, prinsip dasarnya tetap sama.2.4. Koperasi Menunggu
The
AsyncSleep
event danrun
event loop adalah implementasi sepenuhnya bekerja peristiwa waktunya.Ini secara kooperatif beralih di antara masing-masing dari lima coroutine, menangguhkan masing-masing selama 0,1 detik. Meskipun event loop sinkron, event loop masih mengeksekusi pekerjaan dalam 0,5 detik, bukan 2,5 detik. Setiap coroutine memegang status dan bertindak secara independen.
3. Perulangan peristiwa I / O
Perulangan peristiwa yang mendukung
sleep
cocok untuk polling . Namun, menunggu I / O pada pegangan file dapat dilakukan dengan lebih efisien: sistem operasi mengimplementasikan I / O dan dengan demikian mengetahui pegangan mana yang siap. Idealnya, event loop harus mendukung event eksplisit "ready for I / O".3.1. The
select
panggilanPython sudah memiliki antarmuka untuk meminta OS membaca pegangan I / O. Saat dipanggil dengan tuas untuk membaca atau menulis, ia mengembalikan tuas siap untuk membaca atau menulis:
Misalnya, kita dapat
open
membuat file untuk ditulis dan menunggu sampai siap:Setelah memilih kembali,
writeable
berisi file terbuka kami.3.2. Acara I / O dasar
Mirip dengan
AsyncSleep
permintaan tersebut, kita perlu mendefinisikan acara untuk I / O. Denganselect
logika yang mendasarinya , acara tersebut harus mengacu pada objek yang dapat dibaca - misalnyaopen
file. Selain itu, kami menyimpan berapa banyak data untuk dibaca.Seperti
AsyncSleep
kita kebanyakan hanya menyimpan data yang diperlukan untuk panggilan sistem yang mendasarinya. Kali ini,__await__
dapat dilanjutkan beberapa kali - sampai keinginan kitaamount
telah terbaca. Selain itu, kami mendapatkanreturn
hasil I / O, bukan hanya melanjutkan.3.3. Menambahkan event loop dengan read I / O
Basis untuk loop acara kami masih
run
ditentukan sebelumnya. Pertama, kita perlu melacak permintaan baca. Ini bukan lagi jadwal yang diurutkan, kami hanya memetakan permintaan baca ke coroutine.Karena
select.select
mengambil parameter waktu tunggu, kita dapat menggunakannya sebagai penggantitime.sleep
.Ini memberi kita semua file yang dapat dibaca - jika ada, kita menjalankan coroutine yang sesuai. Jika tidak ada, kita harus menunggu cukup lama agar coroutine kita saat ini berjalan.
Akhirnya, kami harus benar-benar mendengarkan permintaan baca.
3.4. Menyatukannya
Di atas adalah sedikit penyederhanaan. Kita perlu melakukan beberapa peralihan ke pola tidur tidak kelaparan jika kita selalu bisa membaca. Kita perlu menangani tidak ada untuk dibaca atau tidak ada yang menunggu. Namun, hasil akhirnya masih cocok dengan 30 LOC.
3.5. Koperasi I / O
The
AsyncSleep
,AsyncRead
danrun
implementasi yang sekarang sepenuhnya fungsional untuk tidur dan / atau membaca. Sama sepertisleepy
, kita bisa mendefinisikan helper untuk menguji bacaan:Dengan menjalankan ini, kita dapat melihat bahwa I / O kita diselingi dengan tugas menunggu:
4. I / O Non-Pemblokiran
Sementara I / O pada file mendapatkan konsepnya, itu tidak benar-benar cocok untuk perpustakaan seperti
asyncio
:select
panggilan selalu kembali untuk file , dan keduanyaopen
danread
dapat memblokir tanpa batas . Ini memblokir semua coroutine dari sebuah event loop - yang buruk. Pustaka sepertiaiofiles
menggunakan utas dan sinkronisasi ke I / O non-pemblokiran palsu dan acara di file.Namun, soket memungkinkan untuk non-pemblokiran I / O - dan latensi yang melekat membuatnya jauh lebih kritis. Saat digunakan dalam event loop, menunggu data dan mencoba kembali dapat digabungkan tanpa memblokir apa pun.
4.1. Peristiwa I / O Non-Pemblokiran
Mirip dengan kami
AsyncRead
, kami dapat menentukan acara suspend-and-read untuk soket. Alih-alih mengambil file, kami mengambil soket - yang tidak boleh memblokir. Juga, kami__await__
menggunakan,socket.recv
bukanfile.read
.Sebaliknya
AsyncRead
,__await__
menjalankan I / O yang benar-benar tidak memblokir. Saat data tersedia, data selalu terbaca. Jika tidak ada data yang tersedia, itu selalu ditangguhkan. Itu berarti event loop hanya diblokir saat kami melakukan pekerjaan yang berguna.4.2. Buka blokir event loop
Sejauh menyangkut loop acara, tidak banyak yang berubah. Peristiwa yang akan didengarkan masih sama dengan untuk file - deskriptor file yang ditandai siap oleh
select
.Pada titik ini, harus jelas bahwa
AsyncRead
danAsyncRecv
merupakan jenis peristiwa yang sama. Kami dapat dengan mudah merefaktornya menjadi satu acara dengan komponen I / O yang dapat ditukar. Akibatnya, event loop, coroutine, dan event dengan rapi memisahkan penjadwal, kode perantara arbitrer dan I / O aktual.4.3. Sisi jelek dari I / O non-pemblokiran
Pada prinsipnya, apa yang harus Anda lakukan saat ini adalah mereplikasi logika
read
as arecv
forAsyncRecv
. Namun, ini jauh lebih buruk sekarang - Anda harus menangani pengembalian awal ketika fungsi memblokir di dalam kernel, tetapi kontrol hasil kepada Anda. Misalnya, membuka koneksi versus membuka file jauh lebih lama:Singkat cerita, yang tersisa adalah beberapa lusin baris penanganan Exception. Peristiwa dan loop peristiwa sudah berfungsi pada saat ini.
Tambahan
Contoh kode di github
sumber
yield self
di AsyncSleep memberi sayaTask got back yield
kesalahan, mengapa demikian? Saya melihat bahwa kode di asyncio.Futures menggunakan itu. Menggunakan hasil panen kosong berfungsi dengan baik.coro
Desugaring Anda secara konseptual benar, tetapi sedikit tidak lengkap.await
tidak menangguhkan tanpa syarat, tetapi hanya jika menemui panggilan pemblokiran. Bagaimana cara mengetahui bahwa panggilan diblokir? Ini ditentukan oleh kode yang sedang menunggu. Misalnya, implementasi socket read yang menunggu dapat diinginkan untuk:Dalam asyncio nyata, kode yang setara mengubah status a
Future
alih - alih mengembalikan nilai ajaib, tetapi konsepnya sama. Jika diadaptasi dengan tepat ke objek seperti generator, kode di atas dapat diubahawait
.Di sisi penelepon, jika coroutine Anda berisi:
Ini menggambarkan sesuatu yang dekat dengan:
Orang yang akrab dengan generator cenderung menggambarkan hal di atas
yield from
yang melakukan suspensi secara otomatis.Rantai suspensi berlanjut hingga ke loop kejadian, yang memperhatikan bahwa coroutine ditangguhkan, menghapusnya dari set yang dapat dijalankan, dan melanjutkan untuk mengeksekusi coroutine yang dapat dijalankan, jika ada. Jika tidak ada coroutine yang dapat dijalankan, loop akan menunggu
select()
hingga deskriptor file yang diinginkan oleh coroutine siap untuk IO. (Event loop mempertahankan pemetaan file-deskriptor-to-coroutine.)Dalam contoh di atas, setelah
select()
memberi tahu loop peristiwa yangsock
dapat dibaca, itu akan ditambahkan kembalicoro
ke set yang dapat dijalankan, sehingga akan dilanjutkan dari titik penangguhan.Dengan kata lain:
Semuanya terjadi di utas yang sama secara default.
Perulangan peristiwa bertanggung jawab untuk menjadwalkan coroutine dan membangunkannya ketika apa pun yang mereka tunggu (biasanya panggilan IO yang biasanya diblokir, atau batas waktu) menjadi siap.
Untuk wawasan tentang loop acara yang mendorong coroutine, saya merekomendasikan ceramah oleh Dave Beazley ini, di mana dia mendemonstrasikan pengkodean loop acara dari awal di depan audiens langsung.
sumber
async.wait_for()
tidak melakukan apa yang seharusnya ... Mengapa masalah besar untuk menambahkan callback ke loop acara dan memberitahukannya untuk memproses berapa pun panggilan balik yang diperlukan, termasuk yang baru saja Anda tambahkan? Frustrasi saya denganasyncio
sebagian karena fakta bahwa konsep yang mendasarinya sangat sederhana, dan, misalnya, Emacs Lisp telah diterapkan selama berabad-abad, tanpa menggunakan kata kunci ... (yaitucreate-async-process
danaccept-process-output
- dan inilah yang dibutuhkan ... (lanjutan)wait_for
berarti tidak melakukan apa yang seharusnya (itu dilakukan, ini adalah coroutine yang harus Anda tunggu), tetapi ekspektasi Anda tidak sesuai dengan tujuan sistem dirancang dan diterapkan. Saya pikir masalah Anda dapat dicocokkan dengan asyncio jika loop acara berjalan di utas terpisah, tetapi saya tidak tahu detail kasus penggunaan Anda dan, sejujurnya, sikap Anda tidak membuatnya menyenangkan untuk membantu Anda.My frustration with asyncio is in part due to the fact that the underlying concept is very simple, and, for example, Emacs Lisp had implementation for ages, without using buzzwords...
- Tidak ada yang menghentikan Anda untuk menerapkan konsep sederhana ini tanpa kata kunci untuk Python :) Mengapa Anda menggunakan asyncio jelek ini? Terapkan milik Anda sendiri dari awal. Misalnya, Anda dapat mulai dengan membuatasync.wait_for()
fungsi Anda sendiri yang melakukan persis seperti yang seharusnya.asyncio
. Tapi, pada prinsipnya, itu bukanlah keputusan yang saya buat. Saya dipaksa menggunakan bahasa sampah melalui en.wikipedia.org/wiki/Ultimatum_game .Itu semua bermuara pada dua tantangan utama yang ditangani asyncio:
Jawaban untuk poin pertama telah ada sejak lama dan disebut loop seleksi . Dalam python, ini diimplementasikan dalam modul penyeleksi .
Pertanyaan kedua terkait dengan konsep coroutine , yaitu fungsi yang dapat menghentikan eksekusinya dan dikembalikan nanti. Dalam python, coroutine diimplementasikan menggunakan generator dan pernyataan yield from . Itulah yang bersembunyi di balik sintaks async / await .
Lebih banyak sumber daya dalam jawaban ini .
EDIT: Mengatasi komentar Anda tentang goroutine:
Persamaan terdekat dengan goroutine di asyncio sebenarnya bukanlah coroutine tetapi sebuah tugas (lihat perbedaannya dalam dokumentasi ). Dalam python, coroutine (atau generator) tidak tahu apa-apa tentang konsep event loop atau I / O. Ini hanyalah sebuah fungsi yang dapat menghentikan penggunaan eksekusinya
yield
sambil mempertahankan statusnya saat ini, sehingga dapat dipulihkan nanti. Theyield from
sintaks memungkinkan untuk chaining mereka dengan cara yang transparan.Sekarang, dalam tugas asyncio, coroutine di bagian paling bawah rantai selalu menghasilkan masa depan . Masa depan ini kemudian menggelembung ke putaran peristiwa, dan diintegrasikan ke dalam mesin bagian dalam. Saat masa depan disetel untuk dilakukan oleh beberapa callback internal lainnya, loop peristiwa dapat memulihkan tugas dengan mengirim masa depan kembali ke rantai coroutine.
EDIT: Mengatasi beberapa pertanyaan di posting Anda:
Tidak, tidak ada yang terjadi di utas. I / O selalu dikelola oleh event loop, kebanyakan melalui deskriptor file. Namun pendaftaran deskriptor file tersebut biasanya disembunyikan oleh coroutine tingkat tinggi, membuat pekerjaan kotor untuk Anda.
I / O adalah panggilan pemblokiran apa pun. Dalam asyncio, semua operasi I / O harus melalui event loop, karena seperti yang Anda katakan, event loop tidak memiliki cara untuk mengetahui bahwa panggilan pemblokiran sedang dilakukan dalam beberapa kode sinkron. Itu berarti Anda tidak seharusnya menggunakan sinkronisasi
open
dalam konteks coroutine. Sebagai gantinya, gunakan pustaka khusus seperti aiofiles yang menyediakan versi asinkronopen
.sumber
yield from
tidak berarti apa-apa.yield from
hanyalah konstruksi sintaks, ini bukan blok bangunan fundamental yang dapat dijalankan komputer. Begitu pula untuk select loop. Ya, coroutine di Go juga menggunakan select loop, tetapi yang saya coba lakukan akan berhasil di Go, tetapi tidak dengan Python. Saya membutuhkan jawaban yang lebih rinci untuk memahami mengapa itu tidak berhasil.asyncio
, bagi saya, akan bermuara pada kode C yang menggambarkan ke mana sintaks Python diterjemahkan.