Hakkında konuşmak async/awaitve asyncioaynı ş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/awaitve asynciobenzeri ç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ı defkarşı async defaçıklık oluşurdu. Gerçek fark, returnkarşı yield. Bundan awaitveya yield frombireysel 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ı
barve için yığın alanı ayırqux
- ilk ifadeyi özyinelemeli olarak yürütün ve sonraki ifadeye atlayın
- bir kerede,
returndeğ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ı
barve için yığın alanı ayırqux
- ilk ifadeyi özyinelemeli olarak yürütün ve sonraki ifadeye atlayın
- bir kerede,
yielddeğ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,
returndeğ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ı returnve (). 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 cofoowrapcofoocofoocofoo
1.4. Coroutines tüm yol boyunca
Oluşturulduğu gibi, yield fromiki 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 rootve coro_bbirbirinizi 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, rootdevam 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 asyncveawait
Açıklama şimdiye kadar açıkça kullandı yieldve yield fromjeneratörlerinin kelime - yatan işlevselliği aynıdır. Yeni Python3.5 sözdizimi asyncve awaitesas 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 forVe async withsen kıracak çünkü ifadeleri ihtiyaç vardır yield from/awaitBare ile zincirini forve withtablolar.
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. awaitSonunda 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: sleepbir koşul doğru olana kadar tekrar tekrar s. Bununla birlikte, normal bir sleepblok 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 , awaitbir 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 sleeptarafından awaitetkinliğ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.sendsonuca kadar koroutini çalıştırır yield.
coroutine = asleep(100)
while True:
print(coroutine.send(None))
time.sleep(0.1)
Bu bize iki AsyncSleepolay verir ve ardından StopIterationkoroutin yapıldığında a. Tek gecikmenin time.sleepdöngüden kaynaklandığına dikkat edin ! Her biri AsyncSleepyalnı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, sleepbir 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 StopIterationve bunları coroutine atayabiliriz. Ancak temel ilke aynı kalır.
2.4. Kooperatif Bekleme
AsyncSleepOlay ve runolay 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, openyazmak 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 writeableaçı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 selectmantıkla, olay okunabilir bir nesneye, örneğin bir opendosyaya 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, returnsadece 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 rundaha ö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.selectbir 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, AsyncReadVe runuygulamalar 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 openve readolabilir 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, aiofilestı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.recvfile.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ı AsyncReadve AsyncRecvaynı 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 readbir şekilde recviç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
BaseEventLoopuygulanmaktadır: github.com/python/cpython/blob/...