Nói về async/await
và asyncio
không phải là điều giống nhau. Đầu tiên là một cấu trúc cơ bản, cấp thấp (coroutines) trong khi cái sau là một thư viện sử dụng các cấu trúc này. Ngược lại, không có câu trả lời cuối cùng duy nhất.
Sau đây là mô tả chung về cách thức async/await
và asyncio
các thư viện giống như hoạt động. Đó là, có thể có các thủ thuật khác ở trên (có ...) nhưng chúng không quan trọng trừ khi bạn tự xây dựng chúng. Sự khác biệt sẽ không đáng kể trừ khi bạn đã biết đủ để không phải hỏi một câu hỏi như vậy.
1. Coroutines so với các chương trình con trong vỏ hạt
Cũng giống như các chương trình con (hàm, thủ tục, ...), coroutines (trình tạo, ...) là một bản tóm tắt của ngăn xếp lệnh gọi và con trỏ lệnh: có một chồng các đoạn mã đang thực thi và mỗi đoạn là một lệnh cụ thể.
Sự phân biệt của def
so với async def
chỉ là để rõ ràng. Sự khác biệt thực tế là return
so với yield
. Từ cái này, await
hoặcyield from
lấy sự khác biệt từ các lệnh gọi riêng lẻ cho toàn bộ ngăn xếp.
1.1. Chương trình con
Một chương trình con đại diện cho một mức ngăn xếp mới để giữ các biến cục bộ và một lần duyệt các lệnh của nó để đi đến kết thúc. Hãy xem xét một chương trình con như sau:
def subfoo(bar):
qux = 3
return qux * bar
Khi bạn chạy nó, điều đó có nghĩa là
- phân bổ không gian ngăn xếp cho
bar
vàqux
- thực hiện đệ quy câu lệnh đầu tiên và chuyển sang câu lệnh tiếp theo
- cùng một lúc
return
, đẩy giá trị của nó vào ngăn xếp đang gọi
- xóa ngăn xếp (1.) và con trỏ hướng dẫn (2.)
Đáng chú ý, 4. có nghĩa là một chương trình con luôn bắt đầu ở cùng một trạng thái. Mọi thứ dành riêng cho chức năng sẽ bị mất khi hoàn thành. Một chức năng không thể được tiếp tục, ngay cả khi có hướng dẫn sau đó return
.
root -\
: \- subfoo --\
:/--<---return --/
|
V
1.2. Coroutines là chương trình con liên tục
Một chương trình điều tra giống như một chương trình con, nhưng có thể thoát ra mà không phá hủy trạng thái của nó. Hãy xem xét một quy trình đăng quang như thế này:
def cofoo(bar):
qux = yield bar # yield marks a break point
return qux
Khi bạn chạy nó, điều đó có nghĩa là
- phân bổ không gian ngăn xếp cho
bar
vàqux
- thực hiện đệ quy câu lệnh đầu tiên và chuyển sang câu lệnh tiếp theo
- cùng một lúc
yield
, đẩy giá trị của nó vào ngăn xếp đang gọi nhưng lưu trữ ngăn xếp và con trỏ lệnh
- sau khi gọi vào
yield
, khôi phục ngăn xếp và con trỏ hướng dẫn và đẩy các đối số tớiqux
- cùng một lúc
return
, đẩy giá trị của nó vào ngăn xếp đang gọi
- xóa ngăn xếp (1.) và con trỏ hướng dẫn (2.)
Lưu ý việc bổ sung 2.1 và 2.2 - một quy trình đăng quang có thể bị tạm dừng và tiếp tục tại các điểm đã xác định trước. Điều này tương tự như cách một chương trình con bị treo trong khi gọi một chương trình con khác. Sự khác biệt là quy trình đăng nhập hoạt động không bị ràng buộc chặt chẽ với ngăn xếp gọi của nó. Thay vào đó, một quy trình đăng ký bị tạm ngưng là một phần của một ngăn xếp riêng biệt, biệt lập.
root -\
: \- cofoo --\
:/--<+--yield --/
| :
V :
Điều này có nghĩa là các coroutines bị treo có thể được lưu trữ hoặc di chuyển tự do giữa các ngăn xếp. Bất kỳ ngăn xếp cuộc gọi nào có quyền truy cập vào chương trình điều tra đều có thể quyết định tiếp tục nó.
1.3. Duyệt qua ngăn xếp cuộc gọi
Cho đến nay, quy trình đăng ký của chúng tôi chỉ đi xuống ngăn xếp cuộc gọi với yield
. Một chương trình con có thể đi xuống và đi lên ngăn xếp cuộc gọi với return
và ()
. Để hoàn thiện, các coroutines cũng cần một cơ chế để đi lên ngăn xếp cuộc gọi. Hãy xem xét một quy trình đăng quang như thế này:
def wrap():
yield 'before'
yield from cofoo()
yield 'after'
Khi bạn chạy nó, điều đó có nghĩa là nó vẫn cấp phát ngăn xếp và con trỏ lệnh giống như một chương trình con. Khi nó tạm dừng, điều đó vẫn giống như lưu trữ một chương trình con.
Tuy nhiên, yield from
hiện cả hai . Nó đình chỉ ngăn xếp và con trỏ hướng dẫn wrap
và chạy cofoo
. Lưu ý rằng wrap
vẫn bị treo cho đến khi cofoo
kết thúc hoàn toàn. Bất cứ khi nào cofoo
tạm ngừng hoặc một cái gì đó được gửi đi, cofoo
được kết nối trực tiếp với ngăn xếp đang gọi.
1.4. Coroutines tất cả các cách xuống
Như được thiết lập, yield from
cho phép kết nối hai phạm vi qua một phạm vi trung gian khác. Khi áp dụng đệ quy, điều đó có nghĩa là phần trên cùng của ngăn xếp có thể được kết nối với phần dưới cùng của ngăn xếp.
root -\
: \-> coro_a -yield-from-> coro_b --\
:/ <-+------------------------yield ---/
| :
:\ --+-- coro_a.send----------yield ---\
: coro_b <-/
Lưu ý rằng root
và coro_b
không biết về nhau. Điều này làm cho coroutines sạch hơn nhiều so với callbacks: coroutines vẫn được xây dựng trên quan hệ 1: 1 giống như các chương trình con. Coroutines tạm ngừng và tiếp tục toàn bộ chuỗi thực thi hiện có của họ cho đến khi có điểm gọi thông thường.
Đáng chú ý, root
có thể có một số lượng tùy ý các quy trình để tiếp tục. Tuy nhiên, nó không bao giờ có thể tiếp tục nhiều hơn một cùng một lúc. Các mạch vành của cùng một gốc là đồng thời nhưng không song song!
1.5. Python async
vàawait
Lời giải thích cho đến nay đã sử dụng rõ ràng từ vựng yield
và yield from
từ vựng của máy phát điện - chức năng cơ bản là giống nhau. Cú pháp Python3.5 mớiasync
và await
tồn tại chủ yếu để rõ ràng.
def foo(): # subroutine?
return None
def foo(): # coroutine?
yield from foofoo() # generator? coroutine?
async def foo(): # coroutine!
await foofoo() # coroutine!
return None
Các câu lệnh async for
và async with
là cần thiết vì bạn sẽ phá vỡyield from/await
chuỗi với các câu lệnh trần for
và with
.
2. Giải phẫu một vòng lặp sự kiện đơn giản
Tự nó, một quy trình điều tra không có khái niệm về việc mang lại quyền kiểm soát một quy trình đăng ký khác . Nó chỉ có thể nhường quyền kiểm soát cho người gọi ở cuối ngăn xếp quy trình. Người gọi này sau đó có thể chuyển sang một quy trình điều tra khác và chạy nó.
Nút gốc này của một số coroutines thường là một vòng lặp sự kiện : khi tạm ngưng, một coroutine mang lại một sự kiện mà nó muốn tiếp tục. Đổi lại, vòng lặp sự kiện có khả năng chờ đợi một cách hiệu quả các sự kiện này xảy ra. Điều này cho phép nó quyết định quy trình đăng ký nào sẽ chạy tiếp theo hoặc cách đợi trước khi tiếp tục.
Thiết kế như vậy ngụ ý rằng có một tập hợp các sự kiện được xác định trước mà vòng lặp hiểu được. Một số điều tra await
lẫn nhau, cho đến khi cuối cùng một sự kiện được thực hiện await
. Sự kiện này có thể giao tiếp trực tiếp với vòng lặp sự kiện bằng yield
điều khiển ing.
loop -\
: \-> coroutine --await--> event --\
:/ <-+----------------------- yield --/
| :
| : # loop waits for event to happen
| :
:\ --+-- send(reply) -------- yield --\
: coroutine <--yield-- event <-/
Điều quan trọng là việc tạm ngừng đăng ký cho phép vòng lặp sự kiện và các sự kiện giao tiếp trực tiếp. Ngăn xếp coroutine trung gian không yêu cầu bất kỳ kiến thức về vòng lặp nào đang chạy nó, cũng như cách các sự kiện hoạt động.
2.1.1. Sự kiện trong thời gian
Sự kiện đơn giản nhất để xử lý là đạt đến một thời điểm. Đây cũng là một khối cơ bản của mã luồng: một luồng lặp đi lặp lại sleep
s cho đến khi một điều kiện là đúng. Tuy nhiên, mộtsleep
khối tự nó thực thi - chúng tôi muốn các coroutines khác không bị chặn. Thay vào đó, chúng tôi muốn cho vòng lặp sự kiện biết khi nào nó sẽ tiếp tục ngăn xếp quy trình đăng quang hiện tại.
2.1.2. Xác định một sự kiện
Một sự kiện chỉ đơn giản là một giá trị mà chúng ta có thể xác định - có thể là thông qua enum, một kiểu hoặc danh tính khác. Chúng ta có thể xác định điều này bằng một lớp đơn giản lưu trữ thời gian mục tiêu của chúng ta. Ngoài việc lưu trữ thông tin sự kiện, chúng tôi có thể cho phép await
một lớp trực tiếp.
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)
Lớp này chỉ lưu trữ sự kiện - nó không cho biết cách thực sự xử lý nó.
Điểm đặc biệt duy nhất là __await__
- nó là những gì await
từ khóa tìm kiếm. Thực tế, nó là một trình lặp nhưng không có sẵn cho máy móc lặp thông thường.
2.2.1. Đang chờ một sự kiện
Bây giờ chúng ta có một sự kiện, các coroutines phản ứng với nó như thế nào? Chúng tôi sẽ có thể thể hiện tương đương sleep
bằng cách nhập await
sự kiện của chúng tôi. Để xem rõ hơn điều gì đang xảy ra, chúng tôi chờ hai lần trong một nửa thời gian:
import time
async def asleep(duration: float):
"""await that ``duration`` seconds pass"""
await AsyncSleep(time.time() + duration / 2)
await AsyncSleep(time.time() + duration / 2)
Chúng tôi có thể trực tiếp khởi tạo và chạy quy trình điều tra này. Tương tự như một trình tạo, sử dụng coroutine.send
chạy chương trình đăng quang cho đến khi yield
kết quả.
coroutine = asleep(100)
while True:
print(coroutine.send(None))
time.sleep(0.1)
Điều này cho chúng ta hai AsyncSleep
sự kiện và sau đó là StopIteration
thời điểm quy trình đăng quang được thực hiện. Lưu ý rằng độ trễ duy nhất là từ time.sleep
trong vòng lặp! Mỗi AsyncSleep
chỉ lưu trữ một phần bù so với thời điểm hiện tại.
2.2.2. Sự kiện + Ngủ
Tại thời điểm này, chúng tôi có hai cơ chế riêng biệt theo ý của chúng tôi:
AsyncSleep
Các sự kiện có thể được tạo ra từ bên trong quy trình điều tra
time.sleep
có thể chờ đợi mà không ảnh hưởng đến các quy trình
Đáng chú ý, hai điều này là trực giao: không có cái nào ảnh hưởng hoặc kích hoạt cái kia. Do đó, chúng tôi có thể đưa ra chiến lược của riêng mình sleep
để đáp ứng sự chậm trễ của an AsyncSleep
.
2.3. Một vòng lặp sự kiện ngây thơ
Nếu chúng ta có một số quy trình , mỗi quy trình có thể cho chúng tôi biết khi nào nó muốn được đánh thức. Sau đó, chúng ta có thể đợi cho đến khi cái đầu tiên trong số chúng muốn được tiếp tục, sau đó cho cái sau, v.v. Đáng chú ý, tại mỗi thời điểm, chúng ta chỉ quan tâm đến cái nào tiếp theo .
Điều này giúp lập lịch trình đơn giản:
- sắp xếp các thói quen theo thời gian thức dậy mong muốn của họ
- chọn cái đầu tiên muốn thức dậy
- đợi cho đến thời điểm này
- chạy quy trình điều tra này
- lặp lại từ 1.
Một triển khai tầm thường không cần bất kỳ khái niệm nâng cao nào. A list
cho phép sắp xếp các coroutines theo ngày. Chờ đợi là thường xuyên time.sleep
. Chạy coroutines hoạt động giống như trước đây với 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])
Tất nhiên, điều này có rất nhiều chỗ để cải thiện. Chúng ta có thể sử dụng một đống cho hàng đợi hoặc một bảng điều phối cho các sự kiện. Chúng tôi cũng có thể tìm nạp các giá trị trả về từStopIteration
và gán chúng cho chương trình đăng quang. Tuy nhiên, nguyên tắc cơ bản vẫn được giữ nguyên.
2.4. Hợp tác xã đang đợi
Sự AsyncSleep
kiện và run
vòng lặp sự kiện là một triển khai hoạt động đầy đủ của các sự kiện được định thời gian.
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)))
Điều này hợp tác chuyển đổi giữa mỗi trong số năm hành trình, tạm dừng mỗi trong 0,1 giây. Mặc dù vòng lặp sự kiện là đồng bộ, nó vẫn thực hiện công việc trong 0,5 giây thay vì 2,5 giây. Mỗi đăng quang giữ trạng thái và hoạt động độc lập.
3. Vòng lặp sự kiện I / O
Một vòng lặp sự kiện hỗ trợ sleep
phù hợp cho việc bỏ phiếu . Tuy nhiên, việc chờ đợi I / O trên một trình xử lý tệp có thể được thực hiện hiệu quả hơn: hệ điều hành thực hiện I / O và do đó biết những xử lý nào đã sẵn sàng. Lý tưởng nhất, một vòng lặp sự kiện nên hỗ trợ một sự kiện rõ ràng "sẵn sàng cho I / O".
3.1. Cuộc select
gọi
Python đã có một giao diện để truy vấn hệ điều hành cho các xử lý I / O đọc. Khi được gọi với các ô điều khiển để đọc hoặc ghi, nó sẽ trả về các ô điều khiển sẵn sàng để đọc hoặc ghi:
readable, writeable, _ = select.select(rlist, wlist, xlist, timeout)
Ví dụ: chúng tôi có thể tạo open
một tệp để ghi và đợi tệp sẵn sàng:
write_target = open('/tmp/foo')
readable, writeable, _ = select.select([], [write_target], [])
Sau khi chọn trả về, writeable
chứa tệp đang mở của chúng tôi.
3.2. Sự kiện I / O cơ bản
Tương tự như AsyncSleep
yêu cầu, chúng ta cần xác định một sự kiện cho I / O. Với select
logic cơ bản , sự kiện phải tham chiếu đến một đối tượng có thể đọc được - chẳng hạn như một open
tệp. Ngoài ra, chúng tôi lưu trữ bao nhiêu dữ liệu cần đọc.
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)
)
Như với AsyncSleep
chúng tôi chủ yếu chỉ lưu trữ dữ liệu cần thiết cho cuộc gọi hệ thống cơ bản. Lần này, __await__
có khả năng được tiếp tục nhiều lần - cho đến khi bạn muốn amount
đọc. Ngoài ra, chúng return
tôi kết quả I / O thay vì chỉ tiếp tục.
3.3. Bổ sung vòng lặp sự kiện với I / O đã đọc
Cơ sở cho vòng lặp sự kiện của chúng ta vẫn là cái run
được xác định trước đó. Đầu tiên, chúng ta cần theo dõi các yêu cầu đọc. Đây không còn là một lịch trình được sắp xếp, chúng tôi chỉ ánh xạ các yêu cầu đọc đến các quy trình.
# new
waiting_read = {} # type: Dict[file, coroutine]
Vì select.select
có tham số thời gian chờ, chúng ta có thể sử dụng nó thay cho time.sleep
.
# old
time.sleep(max(0.0, until - time.time()))
# new
readable, _, _ = select.select(list(reads), [], [])
Điều này cung cấp cho chúng tôi tất cả các tệp có thể đọc được - nếu có, chúng tôi chạy quy trình đăng ký tương ứng. Nếu không có, chúng tôi đã đợi đủ lâu để quy trình đăng quang hiện tại của chúng tôi chạy.
# new - reschedule waiting coroutine, run readable coroutine
if readable:
waiting.append((until, coroutine))
waiting.sort()
coroutine = waiting_read[readable[0]]
Cuối cùng, chúng ta phải thực sự lắng nghe các yêu cầu đọc.
# new
if isinstance(command, AsyncSleep):
...
elif isinstance(command, AsyncRead):
...
3.4. Kết hợp nó lại với nhau
Ở trên là một chút đơn giản hóa. Chúng ta cần thực hiện một số chuyển đổi để không bị đói khi ngủ nếu chúng ta luôn đọc được. Chúng ta cần xử lý việc không có gì để đọc hoặc không có gì để chờ đợi. Tuy nhiên, kết quả cuối cùng vẫn nằm gọn trong 30 LOC.
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. I / O hợp tác
Hiện tại AsyncSleep
, AsyncRead
và run
triển khai có đầy đủ chức năng để ngủ và / hoặc đọc. Tương tự đối với sleepy
, chúng ta có thể xác định một trình trợ giúp để kiểm tra việc đọc:
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'))
Chạy điều này, chúng ta có thể thấy rằng I / O của chúng ta được xen kẽ với nhiệm vụ chờ đợi:
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. I / O không chặn
Mặc dù I / O trên tệp có khái niệm nhưng nó không thực sự phù hợp với một thư viện như asyncio
: lệnh select
gọi luôn trả về cho tệp và cả hai open
và read
có thể chặn vô thời hạn . Điều này chặn tất cả các quy trình của một vòng lặp sự kiện - điều này không tốt. Các thư viện như aiofiles
sử dụng các chuỗi và đồng bộ hóa để giả mạo I / O không chặn và các sự kiện trên tệp.
Tuy nhiên, các socket cho phép I / O không bị chặn - và độ trễ cố hữu của chúng khiến nó trở nên quan trọng hơn nhiều. Khi được sử dụng trong vòng lặp sự kiện, việc chờ dữ liệu và thử lại có thể được gói gọn mà không chặn bất cứ thứ gì.
4.1. Sự kiện I / O không chặn
Tương tự như của chúng tôi AsyncRead
, chúng tôi có thể xác định một sự kiện tạm dừng và đọc cho các ổ cắm. Thay vì lấy một tệp, chúng tôi lấy một ổ cắm - phải không bị chặn. Ngoài ra, chúng tôi __await__
sử dụng socket.recv
thay vì 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)
)
Ngược lại AsyncRead
, __await__
thực hiện I / O thực sự không bị chặn. Khi dữ liệu có sẵn, nó luôn đọc. Khi không có dữ liệu, nó luôn tạm ngừng. Điều đó có nghĩa là vòng lặp sự kiện chỉ bị chặn trong khi chúng tôi thực hiện công việc hữu ích.
4.2. Bỏ chặn vòng lặp sự kiện
Liên quan đến vòng lặp sự kiện, không có gì thay đổi nhiều. Sự kiện để lắng nghe vẫn giống như đối với tệp - một bộ mô tả tệp được đánh dấu là đã sẵn sàng 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
Tại thời điểm này, rõ ràng là AsyncRead
và AsyncRecv
là cùng một loại sự kiện. Chúng tôi có thể dễ dàng cấu trúc lại chúng thành một sự kiện với một thành phần I / O có thể trao đổi. Trên thực tế, vòng lặp sự kiện, các quy trình và sự kiện tách biệt rõ ràng một bộ lập lịch, mã trung gian tùy ý và I / O thực tế.
4.3. Mặt xấu của I / O không chặn
Về nguyên tắc, những gì bạn nên làm tại thời điểm này là lặp lại logic của read
as a recv
for AsyncRecv
. Tuy nhiên, điều này bây giờ còn tệ hơn nhiều - bạn phải xử lý việc trả về sớm khi các hàm chặn bên trong hạt nhân, nhưng lại nhường quyền kiểm soát cho bạn. Ví dụ: mở kết nối so với mở tệp lâu hơn:
# 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
Truyện dài ngắn, những gì còn lại là vài chục dòng xử lý Ngoại lệ. Các sự kiện và vòng lặp sự kiện đã hoạt động tại thời điểm này.
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
Phụ lục
Mã mẫu tại github
BaseEventLoop
được triển khai: github.com/python/cpython/blob/…