Hakkında konuşmak async/await
ve asyncio
aynı şey değil. Birincisi temel, düşük seviyeli bir yapıdır (eşgüdümler), daha sonra ise bu yapıları kullanan bir kitaplıktır. Tersine, tek bir nihai cevap yoktur.
Aşağıda, kitaplıkların nasıl async/await
ve asyncio
benzeri çalıştığına dair genel bir açıklama yer almaktadır . Yani, üstte başka numaralar da olabilir (var ...) ama siz onları kendiniz oluşturmadıkça önemsizdirler. Böyle bir soruyu sormak zorunda kalmayacak kadar bilginiz yoksa, fark önemsiz olmalıdır.
1. Bir somun kabuğundaki alt yordamlara karşı korutinler
Tıpkı alt yordamlar (işlevler, prosedürler, ...) gibi, eşgörünümler (üreteçler, ...) çağrı yığını ve komut işaretçisinin bir soyutlamasıdır: kod parçalarının çalıştırılmasından oluşan bir yığın vardır ve her biri belirli bir talimattadır.
Ayrımı def
karşı async def
açıklık oluşurdu. Gerçek fark, return
karşı yield
. Bundan await
veya yield from
bireysel aramalardan tüm yığınlara kadar farkı alın.
1.1. Altyordamlar
Bir alt rutin, yerel değişkenleri tutmak için yeni bir yığın seviyesini ve bir sona ulaşmak için talimatlarının tek bir geçişini temsil eder. Bunun gibi bir alt rutin düşünün:
def subfoo(bar):
qux = 3
return qux * bar
Çalıştırdığında, bunun anlamı
bar
ve için yığın alanı ayırqux
- ilk ifadeyi özyinelemeli olarak yürütün ve sonraki ifadeye atlayın
- bir kerede,
return
değerini çağıran yığına itin
- yığını (1.) ve komut işaretçisini (2.) temizleyin
Özellikle, 4. bir alt yordamın her zaman aynı durumda başladığı anlamına gelir. İşlevin kendisine özel olan her şey tamamlandığında kaybolur. Sonrasında talimatlar olsa bile bir işleve devam edilemez return
.
root -\
: \- subfoo --\
:/--<---return --/
|
V
1.2. Kalıcı alt yordamlar olarak korutinler
Bir koroutin, bir alt program gibidir, ancak durumunu bozmadan çıkabilir . Bunun gibi bir eşdizim düşünün:
def cofoo(bar):
qux = yield bar # yield marks a break point
return qux
Çalıştırdığında, bunun anlamı
bar
ve için yığın alanı ayırqux
- ilk ifadeyi özyinelemeli olarak yürütün ve sonraki ifadeye atlayın
- bir kerede,
yield
değerini çağıran yığına itin, ancak yığını ve komut işaretçisini saklayın
- Çağırdıktan sonra
yield
, yığını ve yönerge işaretçisini geri yükleyin ve bağımsız değişkenleriqux
- bir kerede,
return
değerini çağıran yığına itin
- yığını (1.) ve komut işaretçisini (2.) temizleyin
2.1 ve 2.2'nin eklendiğine dikkat edin - bir koroutin önceden tanımlanmış noktalarda askıya alınabilir ve devam ettirilebilir. Bu, bir alt yordamın başka bir alt yordamı çağırırken askıya alınmasına benzer. Aradaki fark, aktif coroutinin, çağıran yığına kesin olarak bağlı olmamasıdır. Bunun yerine, askıya alınmış bir koroutin ayrı, izole bir yığının parçasıdır.
root -\
: \- cofoo --\
:/--<+--yield --/
| :
V :
Bu, askıya alınmış eşzamanların serbestçe depolanabileceği veya yığınlar arasında taşınabileceği anlamına gelir. Bir eşdizime erişimi olan herhangi bir çağrı yığını onu devam ettirmeye karar verebilir.
1.3. Çağrı yığınını geçmek
Şimdiye kadar, coroutine'imiz sadece çağrı yığınında aşağı gidiyor yield
. Bir değişmez aşağı gidebilir ve yukarı ile çağrı yığını return
ve ()
. Tamlık için, eşgüdümler ayrıca çağrı yığınını yukarı çıkarmak için bir mekanizmaya ihtiyaç duyar. Bunun gibi bir eşdizim düşünün:
def wrap():
yield 'before'
yield from cofoo()
yield 'after'
Çalıştırdığınızda, bu, yığını ve komut işaretçisini bir alt yordam gibi hala tahsis ettiği anlamına gelir. Askıya alındığında, bu hala bir alt programı depolamak gibidir.
Ancak her ikisini deyield from
yapar . Stack ve yönerge işaretçisini askıya alır ve çalıştırır . Tamamen bitene kadar askıda kalacağını unutmayın . Ne zaman askıya alınırsa veya bir şey gönderilirse, doğrudan çağrı yığınına bağlanır.wrap
cofoo
wrap
cofoo
cofoo
cofoo
1.4. Coroutines tüm yol boyunca
Oluşturulduğu gibi, yield from
iki kapsamın başka bir ara alana bağlanmasına izin verir. Yinelemeli olarak uygulandığında, bu , yığının üst kısmının yığının altına bağlanabileceği anlamına gelir .
root -\
: \-> coro_a -yield-from-> coro_b --\
:/ <-+------------------------yield ---/
| :
:\ --+-- coro_a.send----------yield ---\
: coro_b <-/
Bunu unutmayın root
ve coro_b
birbirinizi tanımayın. Bu, eşgörünümleri geri aramalardan çok daha temiz hale getirir: eşgörünümler, alt yordamlar gibi 1: 1 ilişki üzerine kuruludur. Coroutinler, normal bir çağrı noktasına kadar tüm mevcut yürütme yığınlarını askıya alır ve devam ettirir.
Özellikle, root
devam ettirilecek keyfi sayıda eşdizine sahip olabilir. Yine de, aynı anda birden fazla devam edemez. Aynı kökün eşzamanlıları eşzamanlıdır ancak paralel değildir!
1.5. Python'un async
veawait
Açıklama şimdiye kadar açıkça kullandı yield
ve yield from
jeneratörlerinin kelime - yatan işlevselliği aynıdır. Yeni Python3.5 sözdizimi async
ve await
esas olarak netlik için var.
def foo(): # subroutine?
return None
def foo(): # coroutine?
yield from foofoo() # generator? coroutine?
async def foo(): # coroutine!
await foofoo() # coroutine!
return None
async for
Ve async with
sen kıracak çünkü ifadeleri ihtiyaç vardır yield from/await
Bare ile zincirini for
ve with
tablolar.
2. Basit bir olay döngüsünün anatomisi
Tek başına, bir koroutinin kontrolü başka bir koroutine verme kavramı yoktur . Yalnızca bir coroutine yığınının altındaki arayan için kontrol sağlayabilir. Bu arayan, daha sonra başka bir coroutine geçebilir ve onu çalıştırabilir.
Birkaç eşgüdümün bu kök düğümü genellikle bir olay döngüsüdür : askıya alındığında, bir koroutin, devam ettirmek istediği bir olay verir . Sırayla, olay döngüsü bu olayların gerçekleşmesini verimli bir şekilde bekleyebilir. Bu, daha sonra hangi coroutinin çalışacağına veya devam etmeden önce nasıl bekleyeceğine karar vermesine olanak tanır.
Böyle bir tasarım, döngünün anladığı bir dizi önceden tanımlanmış olay olduğunu ima eder. await
Sonunda bir olay olana kadar, birbirlerinden birkaç koroutine await
. Bu olay , kontrolü devrederek olay döngüsü ile doğrudan iletişim kurabiliryield
.
loop -\
: \-> coroutine --await--> event --\
:/ <-+----------------------- yield --/
| :
| : # loop waits for event to happen
| :
:\ --+-- send(reply) -------- yield --\
: coroutine <--yield-- event <-/
Önemli olan, coroutine süspansiyonunun olay döngüsünün ve olayların doğrudan iletişim kurmasına izin vermesidir. Ara koroutin yığını, hangi döngünün onu çalıştırdığı veya olayların nasıl çalıştığı hakkında herhangi bir bilgi gerektirmez .
2.1.1. Zaman içindeki olaylar
Ele alınması en basit olay, zamanda bir noktaya ulaşmaktır. Bu aynı zamanda iş parçacığı kodunun temel bir bloğudur: sleep
bir koşul doğru olana kadar tekrar tekrar s. Bununla birlikte, normal bir sleep
blok yürütme kendi başına - diğer coroutinlerin engellenmemesini istiyoruz. Bunun yerine, olay döngüsüne geçerli coroutine yığınını ne zaman devam ettirmesi gerektiğini söylemek istiyoruz.
2.1.2. Bir Olay Tanımlama
Bir olay, bir enum, bir tür veya başka bir kimlik aracılığıyla tanımlayabileceğimiz bir değerdir. Bunu, hedef süremizi saklayan basit bir sınıfla tanımlayabiliriz. Olay bilgilerini saklamaya ek olarak , await
bir sınıfa doğrudan izin verebiliriz .
class AsyncSleep:
"""Event to sleep until a point in time"""
def __init__(self, until: float):
self.until = until
# used whenever someone ``await``s an instance of this Event
def __await__(self):
# yield this Event to the loop
yield self
def __repr__(self):
return '%s(until=%.1f)' % (self.__class__.__name__, self.until)
Bu sınıf yalnızca olayı depolar - gerçekten nasıl ele alınacağını söylemez.
Tek özel özellik, anahtar kelimenin aradığı __await__
şeydir await
. Pratik olarak, bir yineleyicidir, ancak normal yineleme makineleri için mevcut değildir.
2.2.1. Bir olay bekleniyor
Artık bir olayımız olduğuna göre, koroutinler buna nasıl tepki veriyor? Biz eşdeğer ifade etmek gerekir sleep
tarafından await
etkinliğimize ing. Neler olup bittiğini daha iyi görmek için, zamanın yarısında iki kez bekleriz:
import time
async def asleep(duration: float):
"""await that ``duration`` seconds pass"""
await AsyncSleep(time.time() + duration / 2)
await AsyncSleep(time.time() + duration / 2)
Bu coroutini doğrudan somutlaştırabilir ve çalıştırabiliriz. Bir jeneratöre benzer şekilde, kullanmak bir coroutine.send
sonuca kadar koroutini çalıştırır yield
.
coroutine = asleep(100)
while True:
print(coroutine.send(None))
time.sleep(0.1)
Bu bize iki AsyncSleep
olay verir ve ardından StopIteration
koroutin yapıldığında a. Tek gecikmenin time.sleep
döngüden kaynaklandığına dikkat edin ! Her biri AsyncSleep
yalnızca geçerli zamana göre bir fark saklar.
2.2.2. Olay + Uyku
Bu noktada elimizde iki ayrı mekanizma var :
AsyncSleep
Bir koroutin içinden verilebilen olaylar
time.sleep
eşgüdümleri etkilemeden bekleyebilen
Özellikle, bu ikisi ortogonaldir: hiçbiri diğerini etkilemez veya tetiklemez. Sonuç olarak, sleep
bir gecikmeyi karşılamak için kendi stratejimizi geliştirebiliriz AsyncSleep
.
2.3. Saf bir olay döngüsü
Mecbur kalırsak birkaç coroutines o Uyandırılmak istediğinde, her söyleyebilir. Daha sonra, birincisinin devam ettirilmesini isteyene kadar bekleyebiliriz, sonra bir sonrakini vb. Özellikle, her noktada yalnızca hangisinin bir sonraki olduğunu önemsiyoruz .
Bu, basit bir zamanlama sağlar:
- eşgüdümleri istenen uyanma zamanına göre sıralayın
- uyanmak isteyen ilk kişiyi seç
- bu zamana kadar bekle
- bu eşdizimi çalıştır
- 1'den itibaren tekrarlayın.
Önemsiz bir uygulama herhangi bir gelişmiş kavrama ihtiyaç duymaz. A list
, eşdizimleri tarihe göre sıralamayı sağlar. Beklemek normaldir time.sleep
. Eşgörünümleri çalıştırmak eskisi gibi çalışır coroutine.send
.
def run(*coroutines):
"""Cooperatively run all ``coroutines`` until completion"""
# store wake-up-time and coroutines
waiting = [(0, coroutine) for coroutine in coroutines]
while waiting:
# 2. pick the first coroutine that wants to wake up
until, coroutine = waiting.pop(0)
# 3. wait until this point in time
time.sleep(max(0.0, until - time.time()))
# 4. run this coroutine
try:
command = coroutine.send(None)
except StopIteration:
continue
# 1. sort coroutines by their desired suspension
if isinstance(command, AsyncSleep):
waiting.append((command.until, coroutine))
waiting.sort(key=lambda item: item[0])
Tabii ki, burada iyileştirme için bolca yer var. Bekleme kuyruğu için bir yığın veya olaylar için bir gönderim tablosu kullanabiliriz. Ayrıca, dönüş değerlerini de alabilir StopIteration
ve bunları coroutine atayabiliriz. Ancak temel ilke aynı kalır.
2.4. Kooperatif Bekleme
AsyncSleep
Olay ve run
olay döngü zamanlanmış olaylar tamamen çalışma uygulaması vardır.
async def sleepy(identifier: str = "coroutine", count=5):
for i in range(count):
print(identifier, 'step', i + 1, 'at %.2f' % time.time())
await asleep(0.1)
run(*(sleepy("coroutine %d" % j) for j in range(5)))
Bu, beş eş çizginin her biri arasında işbirliği yaparak geçiş yapar ve her birini 0,1 saniye askıya alır. Olay döngüsü eşzamanlı olsa da işi 2,5 saniye yerine 0,5 saniyede yürütür. Her bir coroutine durumu tutar ve bağımsız olarak hareket eder.
3. G / Ç olay döngüsü
Destekleyen bir olay döngüsü yoklamasleep
için uygundur . Bununla birlikte, bir dosya tanıtıcısı üzerinde G / Ç beklemesi daha verimli bir şekilde yapılabilir: işletim sistemi G / Ç uygular ve bu nedenle hangi tanıtıcıların hazır olduğunu bilir. İdeal olarak, bir olay döngüsü açık bir "G / Ç için hazır" olayını desteklemelidir.
3.1. select
çağrı
Python, işletim sistemini okuma G / Ç tutamaçlarını sorgulamak için zaten bir arayüze sahiptir. Okumak veya yazmak için tutamaçlarla çağrıldığında, tutamaçları okumaya veya yazmaya hazır döndürür :
readable, writeable, _ = select.select(rlist, wlist, xlist, timeout)
Örneğin, open
yazmak için bir dosya hazırlayabiliriz ve hazır olmasını bekleyebiliriz:
write_target = open('/tmp/foo')
readable, writeable, _ = select.select([], [write_target], [])
Select döndükten sonra writeable
açık dosyamızı içerir.
3.2. Temel G / Ç olayı
AsyncSleep
İsteğe benzer şekilde, G / Ç için bir olay tanımlamamız gerekir. Temeldeki select
mantıkla, olay okunabilir bir nesneye, örneğin bir open
dosyaya başvurmalıdır . Ek olarak, ne kadar veri okunacağını da saklıyoruz.
class AsyncRead:
def __init__(self, file, amount=1):
self.file = file
self.amount = amount
self._buffer = ''
def __await__(self):
while len(self._buffer) < self.amount:
yield self
# we only get here if ``read`` should not block
self._buffer += self.file.read(1)
return self._buffer
def __repr__(self):
return '%s(file=%s, amount=%d, progress=%d)' % (
self.__class__.__name__, self.file, self.amount, len(self._buffer)
)
Olduğu gibi, AsyncSleep
çoğunlukla temeldeki sistem çağrısı için gereken verileri depoluyoruz. Bu sefer, __await__
istediğimiz okunana kadar birden çok kez devam ettirilebilir amount
. Ek olarak, return
sadece devam etmek yerine I / O sonucunu veriyoruz.
3.3. Okuma G / Ç ile bir olay döngüsünü genişletme
Olay döngümüzün temeli hala run
daha önce tanımlanmıştır. Öncelikle okuma isteklerini takip etmemiz gerekiyor. Bu artık sıralı bir program değil, sadece okuma isteklerini eşgüdümlerle eşleştiriyoruz.
# new
waiting_read = {} # type: Dict[file, coroutine]
Yana select.select
bir zaman aşımı parametre alır, biz yerine kullanabilirsiniz time.sleep
.
# old
time.sleep(max(0.0, until - time.time()))
# new
readable, _, _ = select.select(list(reads), [], [])
Bu bize okunabilir tüm dosyaları verir - eğer varsa, karşılık gelen coroutini çalıştırırız. Hiçbiri yoksa, mevcut koroutinimizin çalışması için yeterince uzun süre bekledik.
# new - reschedule waiting coroutine, run readable coroutine
if readable:
waiting.append((until, coroutine))
waiting.sort()
coroutine = waiting_read[readable[0]]
Son olarak, okuma isteklerini gerçekten dinlemeliyiz.
# new
if isinstance(command, AsyncSleep):
...
elif isinstance(command, AsyncRead):
...
3.4. Bir araya getirmek
Yukarıdakiler biraz basitleştirmedir. Her zaman okuyabiliyorsak, uyku eşgüdümlerini aç bırakmamak için biraz geçiş yapmalıyız. Okumak ya da bekleyecek hiçbir şey olmamasını halletmemiz gerekiyor. Ancak, sonuç yine de 30 LOC'ye uyuyor.
def run(*coroutines):
"""Cooperatively run all ``coroutines`` until completion"""
waiting_read = {} # type: Dict[file, coroutine]
waiting = [(0, coroutine) for coroutine in coroutines]
while waiting or waiting_read:
# 2. wait until the next coroutine may run or read ...
try:
until, coroutine = waiting.pop(0)
except IndexError:
until, coroutine = float('inf'), None
readable, _, _ = select.select(list(waiting_read), [], [])
else:
readable, _, _ = select.select(list(waiting_read), [], [], max(0.0, until - time.time()))
# ... and select the appropriate one
if readable and time.time() < until:
if until and coroutine:
waiting.append((until, coroutine))
waiting.sort()
coroutine = waiting_read.pop(readable[0])
# 3. run this coroutine
try:
command = coroutine.send(None)
except StopIteration:
continue
# 1. sort coroutines by their desired suspension ...
if isinstance(command, AsyncSleep):
waiting.append((command.until, coroutine))
waiting.sort(key=lambda item: item[0])
# ... or register reads
elif isinstance(command, AsyncRead):
waiting_read[command.file] = coroutine
3.5. Kooperatif G / Ç
AsyncSleep
, AsyncRead
Ve run
uygulamalar artık uyku ve / veya okuma tamamen işlevseldir. Aynı şekilde sleepy
, okumayı test etmek için bir yardımcı tanımlayabiliriz:
async def ready(path, amount=1024*32):
print('read', path, 'at', '%d' % time.time())
with open(path, 'rb') as file:
result = return await AsyncRead(file, amount)
print('done', path, 'at', '%d' % time.time())
print('got', len(result), 'B')
run(sleepy('background', 5), ready('/dev/urandom'))
Bunu çalıştırarak, G / Ç'mizin bekleme göreviyle karıştırıldığını görebiliriz:
id background round 1
read /dev/urandom at 1530721148
id background round 2
id background round 3
id background round 4
id background round 5
done /dev/urandom at 1530721148
got 1024 B
4. Engellemesiz G / Ç
G / Ç dosyalar üzerinde kavramını karşısında alırken, bu gibi bir kütüphane için gerçekten uygun değildir asyncio
: select
çağrı dosyaları için her zaman döner ve her ikisi de open
ve read
olabilir süresiz bloke . Bu, bir olay döngüsünün tüm koroutinlerini engeller - ki bu kötüdür. Bu gibi kitaplıklar, aiofiles
tıkanmayan G / Ç ve dosyadaki olayları taklit etmek için iş parçacıkları ve senkronizasyon kullanır.
Bununla birlikte, soketler engellemeyen G / Ç'ye izin verir ve doğal gecikmeleri onu çok daha kritik hale getirir. Bir olay döngüsünde kullanıldığında, veri beklemek ve yeniden denemek, hiçbir şeyi engellemeden sarılabilir.
4.1. Engellemeyen G / Ç olayı
Bizimkine benzer şekilde AsyncRead
, soketler için bir askıya alma ve okuma olayı tanımlayabiliriz. Bir dosya almak yerine, bloke olmaması gereken bir soket alıyoruz. Ayrıca, yerine __await__
kullanımlarımız .socket.recv
file.read
class AsyncRecv:
def __init__(self, connection, amount=1, read_buffer=1024):
assert not connection.getblocking(), 'connection must be non-blocking for async recv'
self.connection = connection
self.amount = amount
self.read_buffer = read_buffer
self._buffer = b''
def __await__(self):
while len(self._buffer) < self.amount:
try:
self._buffer += self.connection.recv(self.read_buffer)
except BlockingIOError:
yield self
return self._buffer
def __repr__(self):
return '%s(file=%s, amount=%d, progress=%d)' % (
self.__class__.__name__, self.connection, self.amount, len(self._buffer)
)
Bunun aksine AsyncRead
, __await__
gerçekten engellemeyen G / Ç gerçekleştirir. Veriler mevcut olduğunda her zaman okur. Mevcut veri olmadığında, her zaman askıya alınır. Bu, olay döngüsünün yalnızca yararlı işler yaparken engellendiği anlamına gelir.
4.2. Olay döngüsünün engellenmesini kaldırma
Olay döngüsü söz konusu olduğunda, hiçbir şey pek değişmez. Dinlenecek olay dosyalar için olanla aynıdır - hazır olarak işaretlenmiş bir dosya tanımlayıcısı select
.
# old
elif isinstance(command, AsyncRead):
waiting_read[command.file] = coroutine
# new
elif isinstance(command, AsyncRead):
waiting_read[command.file] = coroutine
elif isinstance(command, AsyncRecv):
waiting_read[command.connection] = coroutine
Bu noktada, açık olmalı AsyncRead
ve AsyncRecv
aynı tür olaylardır. Değiştirilebilir bir G / Ç bileşenine sahip tek bir olay olarak bunları kolayca yeniden düzenleyebiliriz . Gerçekte, olay döngüsü, eşgüdümler ve olaylar açıkça birbirinden ayrılır bir programlayıcıyı, rastgele ara kodu ve gerçek G / Ç'yi bir .
4.3. Engellemeyen G / Ç'nin çirkin tarafı
Prensip olarak, ne bu noktada yapması gerektiğini mantığını çoğaltmak olduğu read
bir şekilde recv
için AsyncRecv
. Ancak, bu şimdi çok daha çirkin - işlevler çekirdek içinde engellendiğinde erken dönüşleri halletmelisiniz, ancak kontrolü size vermelisiniz. Örneğin, bir bağlantıyı açmak yerine bir dosyayı açmak çok daha uzundur:
# file
file = open(path, 'rb')
# non-blocking socket
connection = socket.socket()
connection.setblocking(False)
# open without blocking - retry on failure
try:
connection.connect((url, port))
except BlockingIOError:
pass
Uzun lafın kısası, geriye kalan birkaç düzine İstisna işleme hattı. Olaylar ve olay döngüsü bu noktada zaten çalışıyor.
id background round 1
read localhost:25000 at 1530783569
read /dev/urandom at 1530783569
done localhost:25000 at 1530783569 got 32768 B
id background round 2
id background round 3
id background round 4
done /dev/urandom at 1530783569 got 4096 B
id background round 5
Ek
Github'daki örnek kod
BaseEventLoop
uygulanmaktadır: github.com/python/cpython/blob/...