Asyncio thực sự hoạt động như thế nào?


118

Câu hỏi này được thúc đẩy bởi một câu hỏi khác của tôi: Làm thế nào để chờ đợi trong cdef?

Có rất nhiều bài báo và bài đăng blog trên web về asyncio, nhưng chúng đều rất hời hợt. Tôi không thể tìm thấy bất kỳ thông tin nào về cách asynciothực sự được triển khai và điều gì khiến I / O không đồng bộ. Tôi đã cố gắng đọc mã nguồn, nhưng đó là hàng nghìn dòng không phải là mã cấp C cao nhất, rất nhiều trong số đó liên quan đến các đối tượng phụ trợ, nhưng quan trọng nhất, thật khó để kết nối giữa cú pháp Python và mã C mà nó sẽ dịch. thành.

Tài liệu riêng của Asycnio thậm chí còn ít hữu ích hơn. Không có thông tin ở đó về cách nó hoạt động, chỉ có một số hướng dẫn về cách sử dụng nó, đôi khi cũng gây hiểu lầm / viết rất kém.

Tôi quen với việc triển khai các coroutines của Go và hy vọng rằng Python cũng làm được điều tương tự. Nếu đúng như vậy, mã tôi đưa ra trong bài đăng được liên kết ở trên sẽ hoạt động. Vì nó không xảy ra, nên bây giờ tôi đang cố gắng tìm hiểu tại sao. Dự đoán tốt nhất của tôi cho đến nay là như sau, vui lòng sửa cho tôi nơi tôi sai:

  1. Các định nghĩa thủ tục của biểu mẫu async def foo(): ...thực sự được hiểu là các phương thức của một lớp kế thừa coroutine.
  2. Có lẽ, async defthực sự được chia thành nhiều phương thức bằng các awaitcâu lệnh, trong đó đối tượng mà các phương thức này được gọi có thể theo dõi tiến trình mà nó đã đạt được thông qua việc thực thi cho đến nay.
  3. Nếu điều trên là đúng, thì về cơ bản, việc thực thi một coroutine chỉ gọi các phương thức của đối tượng coroutine bởi một trình quản lý toàn cục nào đó (vòng lặp?).
  4. Trình quản lý toàn cầu bằng cách nào đó (bằng cách nào?) Biết khi nào các hoạt động I / O được thực hiện bởi mã Python (chỉ?) Và có thể chọn một trong các phương thức đăng ký đang chờ xử lý để thực thi sau khi phương thức thực thi hiện tại bị loại bỏ quyền kiểm soát (nhấn vào awaitcâu lệnh ).

Nói cách khác, đây là nỗ lực của tôi trong việc "giải mã" một số asynciocú pháp thành một thứ dễ hiểu hơn:

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()

Nếu suy đoán của tôi chứng minh là đúng: thì tôi có một vấn đề. I / O thực sự xảy ra như thế nào trong trường hợp này? Trong một chủ đề riêng biệt? Toàn bộ thông dịch viên có bị tạm ngưng và I / O xảy ra bên ngoài thông dịch viên không? Chính xác thì I / O có nghĩa là gì? Nếu thủ tục python của tôi được gọi là thủ tục C open()và đến lượt nó gửi ngắt đến hạt nhân, từ bỏ quyền kiểm soát đối với nó, trình thông dịch Python biết về điều này và có thể tiếp tục chạy một số mã khác, trong khi mã hạt nhân thực hiện I / O thực tế và cho đến khi nó đánh thức thủ tục Python đã gửi ngắt ban đầu? Làm thế nào để trình thông dịch Python về nguyên tắc, nhận thức được điều này đang xảy ra?


2
Hầu hết logic được xử lý bởi việc triển khai vòng lặp sự kiện. Xem cách CPython BaseEventLoopđược triển khai: github.com/python/cpython/blob/…
Blender

@Blender ok, tôi nghĩ rằng cuối cùng tôi đã tìm thấy những gì tôi muốn, nhưng bây giờ tôi không hiểu lý do tại sao mã lại được viết như vậy. Tại sao _run_once, thực sự là chức năng hữu ích duy nhất trong toàn bộ mô-đun này được đặt ở chế độ "riêng tư"? Việc triển khai thật kinh khủng, nhưng đó ít vấn đề hơn. Tại sao hàm duy nhất bạn muốn gọi trên vòng lặp sự kiện được đánh dấu là "đừng gọi cho tôi"?
wvxvw

Đó là một câu hỏi cho danh sách gửi thư. Trường hợp sử dụng nào sẽ yêu cầu bạn chạm _run_oncevào ngay từ đầu?
Máy xay sinh tố

8
Tuy nhiên, điều đó không thực sự trả lời câu hỏi của tôi. Làm thế nào bạn sẽ giải quyết bất kỳ vấn đề hữu ích bằng cách sử dụng chỉ _run_once? asynciolà phức tạp và có lỗi của nó, nhưng hãy giữ cho cuộc thảo luận dân sự. Đừng nói xấu các nhà phát triển đằng sau mã mà chính bạn không hiểu.
Máy xay sinh tố

1
@ user8371915 Nếu bạn tin rằng có bất kỳ điều gì tôi chưa đề cập, bạn có thể thêm hoặc nhận xét về câu trả lời của tôi.
Bharel

Câu trả lời:


202

Asyncio hoạt động như thế nào?

Trước khi trả lời câu hỏi này, chúng ta cần hiểu một số thuật ngữ cơ bản, hãy bỏ qua chúng nếu bạn đã biết bất kỳ thuật ngữ nào.

Máy phát điện

Trình tạo là các đối tượng cho phép chúng ta tạm dừng việc thực thi một hàm python. Trình tạo do người dùng quản lý đang triển khai bằng cách sử dụng từ khóa yield. Bằng cách tạo một hàm bình thường có chứa yieldtừ khóa, chúng tôi biến hàm đó thành một trình tạo:

>>> def test():
...     yield 1
...     yield 2
...
>>> gen = test()
>>> next(gen)
1
>>> next(gen)
2
>>> next(gen)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

Như bạn có thể thấy, việc gọi next()trình tạo sẽ khiến trình thông dịch tải khung của thử nghiệm và trả về yieldgiá trị ed. Gọi next()lại, làm cho khung tải lại vào ngăn xếp trình thông dịch và tiếp tục nhập yieldmột giá trị khác.

Đến lần thứ ba next()được gọi, máy phát điện của chúng tôi đã hoàn thành và StopIterationđược ném.

Giao tiếp với máy phát điện

Một tính năng ít được biết đến của máy phát điện, là bạn có thể giao tiếp với chúng bằng hai phương pháp: send()throw().

>>> def test():
...     val = yield 1
...     print(val)
...     yield 2
...     yield 3
...
>>> gen = test()
>>> next(gen)
1
>>> gen.send("abc")
abc
2
>>> gen.throw(Exception())
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 4, in test
Exception

Khi gọi gen.send(), giá trị được chuyển dưới dạng giá trị trả về từ từ yieldkhóa.

gen.throw()mặt khác, cho phép ném Ngoại lệ bên trong máy phát điện, với ngoại lệ được nêu ra tại cùng một điểm yieldđã được gọi.

Trả về giá trị từ trình tạo

Trả về một giá trị từ trình tạo, dẫn đến giá trị được đặt bên trong StopIterationngoại lệ. Sau đó, chúng tôi có thể khôi phục giá trị từ ngoại lệ và sử dụng nó theo nhu cầu của chúng tôi.

>>> def test():
...     yield 1
...     return "abc"
...
>>> gen = test()
>>> next(gen)
1
>>> try:
...     next(gen)
... except StopIteration as exc:
...     print(exc.value)
...
abc

Kìa, một từ khóa mới: yield from

Python 3.4 ra đời với việc bổ sung một từ khóa mới: yield from . Có gì từ khóa cho phép chúng ta làm, là vượt qua trên bất kỳ next(), send()throw()vào một máy phát điện nội nhất lồng nhau. Nếu trình tạo bên trong trả về một giá trị, nó cũng là giá trị trả về của yield from:

>>> def inner():
...     inner_result = yield 2
...     print('inner', inner_result)
...     return 3
...
>>> def outer():
...     yield 1
...     val = yield from inner()
...     print('outer', val)
...     yield 4
...
>>> gen = outer()
>>> next(gen)
1
>>> next(gen) # Goes inside inner() automatically
2
>>> gen.send("abc")
inner abc
outer 3
4

Tôi đã viết một bài báo để giải thích thêm về chủ đề này.

Để tất cả chúng cùng nhau

Khi giới thiệu từ khóa mới yield fromtrong Python 3.4, giờ đây chúng tôi đã có thể tạo các bộ tạo bên trong các bộ tạo giống như một đường hầm, truyền dữ liệu qua lại từ bộ tạo bên trong nhất đến bên ngoài nhất. Điều này đã tạo ra một ý nghĩa mới cho máy phát điện - coroutines .

Coroutines là những chức năng có thể dừng và tiếp tục trong khi đang chạy. Trong Python, chúng được định nghĩa bằng async deftừ khóa. Giống như máy phát điện, chúng cũng sử dụng hình thức riêng của yield fromchúng await. Trước asyncawaitđã được giới thiệu trong Python 3.5, chúng tôi tạo ra coroutines trong máy phát điện cùng một cách chính xác được tạo ra (vớiyield from thay vì await).

async def inner():
    return 1

async def outer():
    await inner()

Giống như mọi trình lặp hoặc trình tạo triển khai __iter__() phương thức, thực thi coroutines __await__()cho phép chúng tiếp tục mỗi khi await corođược gọi.

Có một sơ đồ trình tự đẹp bên trong các tài liệu Python mà bạn nên xem.

Trong asyncio, ngoài các hàm coroutine, chúng ta có 2 đối tượng quan trọng: tasktương lai .

Hợp đồng tương lai

Tương lai là các đối tượng có __await__()phương thức được thực hiện, và công việc của chúng là giữ một trạng thái và kết quả nhất định. Trạng thái có thể là một trong những trạng thái sau:

  1. PENDING - tương lai không có bất kỳ kết quả hoặc thiết lập ngoại lệ nào.
  2. CANCELED - tương lai đã bị hủy bằng cách sử dụng fut.cancel()
  3. FINISHED - tương lai đã kết thúc, bởi một tập hợp kết quả sử dụng fut.set_result()hoặc bởi một tập hợp ngoại lệ sử dụngfut.set_exception()

Kết quả, giống như bạn đã đoán, có thể là một đối tượng Python, sẽ được trả về hoặc một ngoại lệ có thể được đưa ra.

Một tính năng quan trọng khác của futurecác đối tượng là chúng chứa một phương thức được gọi làadd_done_callback() . Phương thức này cho phép các hàm được gọi ngay sau khi tác vụ được thực hiện - cho dù nó đã nêu ra một ngoại lệ hay đã kết thúc.

Nhiệm vụ

Các đối tượng tác vụ là các tương lai đặc biệt, bao quanh các coroutines và giao tiếp với các coroutines bên trong nhất và bên ngoài nhất. Mỗi khi đăng quang một awaittương lai, tương lai được chuyển qua tất cả các cách trở lại nhiệm vụ (giống như trong yield from) và tác vụ nhận được nó.

Tiếp theo, nhiệm vụ tự ràng buộc với tương lai. Nó làm như vậy bằng cách kêu gọi add_done_callback()tương lai. Kể từ bây giờ, nếu tương lai sẽ được thực hiện, bằng cách bị hủy, chuyển một ngoại lệ hoặc kết quả là chuyển một đối tượng Python, thì lệnh gọi lại của nhiệm vụ sẽ được gọi và nó sẽ tồn tại trở lại.

Asyncio

Câu hỏi cuối cùng mà chúng ta phải trả lời là - IO được thực hiện như thế nào?

Sâu bên trong asyncio, chúng ta có một vòng lặp sự kiện. Một vòng lặp sự kiện của các nhiệm vụ. Công việc của vòng lặp sự kiện là gọi các tác vụ mỗi khi chúng sẵn sàng và phối hợp tất cả nỗ lực đó vào một máy làm việc duy nhất.

Phần IO của vòng lặp sự kiện được xây dựng dựa trên một hàm quan trọng duy nhất được gọi select. Select là một chức năng chặn, được thực hiện bởi hệ điều hành bên dưới, cho phép chờ dữ liệu đến hoặc đi trên các ổ cắm. Sau khi nhận được dữ liệu, nó sẽ đánh thức và trả về các ổ cắm đã nhận dữ liệu hoặc các ổ cắm đã sẵn sàng để ghi.

Khi bạn cố gắng nhận hoặc gửi dữ liệu qua ổ cắm thông qua asyncio, điều thực sự xảy ra bên dưới là ổ cắm được kiểm tra lần đầu tiên nếu nó có bất kỳ dữ liệu nào có thể đọc hoặc gửi ngay lập tức. Nếu .send()bộ đệm của nó đầy hoặc .recv()bộ đệm trống, ổ cắm được đăng ký vào selecthàm (chỉ cần thêm nó vào một trong các danh sách, rlistcho recvwlistcho send) và hàm thích hợp awaitsa mới được tạofuture đối tượng , được gắn với ổ cắm đó.

Khi tất cả các nhiệm vụ có sẵn đang chờ trong tương lai, vòng lặp sự kiện sẽ gọi selectvà chờ. Khi một trong các ổ cắm có dữ liệu đến hoặc sendbộ đệm của nó bị cạn kiệt, asyncio sẽ kiểm tra đối tượng tương lai được gắn với ổ cắm đó và đặt nó thành hoàn tất.

Bây giờ tất cả điều kỳ diệu xảy ra. Tương lai được thiết lập để hoàn thành, nhiệm vụ đã tự thêm vào trước đó add_done_callback()sẽ hoạt động trở lại và gọi .send()quy trình đăng ký tiếp tục quy trình đăng quang bên trong nhất (vì awaitchuỗi) và bạn đọc dữ liệu mới nhận được từ bộ đệm gần đó. đã bị tràn vào.

Chuỗi phương pháp một lần nữa, trong trường hợp recv():

  1. select.select chờ đợi.
  2. Một ổ cắm đã sẵn sàng, với dữ liệu được trả về.
  3. Dữ liệu từ ổ cắm được chuyển vào bộ đệm.
  4. future.set_result() được gọi là.
  5. Tác vụ đã thêm chính nó add_done_callback()giờ đã được đánh thức.
  6. Nhiệm vụ gọi .send()đến quy trình đăng ký đi vào tất cả các quy trình đăng ký bên trong nhất và đánh thức nó.
  7. Dữ liệu đang được đọc từ bộ đệm và được trả lại cho người dùng khiêm tốn của chúng tôi.

Tóm lại, asyncio sử dụng các khả năng của trình tạo, cho phép tạm dừng và tiếp tục các chức năng. Nó sử dụng các yield fromkhả năng cho phép truyền dữ liệu qua lại từ bộ tạo bên trong nhất ra bên ngoài. Nó sử dụng tất cả những thứ đó để tạm dừng thực thi chức năng trong khi chờ IO hoàn thành (bằng cách sử dụng selectchức năng OS ).

Và tốt nhất của tất cả? Trong khi một chức năng bị tạm dừng, chức năng khác có thể chạy và xen vào với loại vải mỏng manh, đó là asyncio.


12
Nếu cần giải thích thêm, đừng ngần ngại comment. Btw, tôi không hoàn toàn chắc chắn liệu tôi có nên viết bài này như một bài báo trên blog hay một câu trả lời trong stackoverflow hay không. Câu hỏi là một câu hỏi dài để trả lời.
Bharel

1
Trên ổ cắm không đồng bộ, việc cố gắng gửi hoặc nhận dữ liệu sẽ kiểm tra bộ đệm hệ điều hành trước. Nếu bạn đang cố gắng nhận và không có dữ liệu nào trong bộ đệm, thì hàm nhận bên dưới sẽ trả về giá trị lỗi sẽ phổ biến như một ngoại lệ trong Python. Tương tự với gửi và một bộ đệm đầy đủ. Khi ngoại lệ được nâng lên, Python lần lượt gửi các socket đó đến hàm select để tạm dừng quá trình. Nhưng không phải asyncio hoạt động như thế nào, mà là cách select và socket hoạt động cũng là những thứ rất đặc trưng cho hệ điều hành.
Bharel

2
@ user8371915 Luôn ở đây để trợ giúp :-) Hãy nhớ rằng để hiểu Asyncio, bạn phải biết cách máy phát điện, giao tiếp và yield fromhoạt động của máy phát điện . Tuy nhiên, tôi đã lưu ý hàng đầu rằng nó có thể bỏ qua trong trường hợp người đọc đã biết về nó :-) Bạn có tin rằng tôi nên thêm gì nữa không?
Bharel

2
Những điều trước phần Asyncio có lẽ là quan trọng nhất, vì chúng là điều duy nhất mà ngôn ngữ thực sự làm được. Nó selectcũng có thể đủ điều kiện, vì đó là cách các lệnh gọi hệ thống I / O không chặn hoạt động trên OS. Các asynciocấu trúc thực tế và vòng lặp sự kiện chỉ là mã cấp ứng dụng được xây dựng từ những thứ này.
MisterMiyagi

3
Bài đăng này có thông tin về xương sống của I / O không đồng bộ trong Python. Cảm ơn vì một lời giải thích tử tế
mjkim

83

Nói về async/awaitasynciokhô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/awaitasynciocá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 defso với async defchỉ là để rõ ràng. Sự khác biệt thực tế là returnso với yield. Từ cái này, awaithoặ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à

  1. phân bổ không gian ngăn xếp cho barqux
  2. thực hiện đệ quy câu lệnh đầu tiên và chuyển sang câu lệnh tiếp theo
  3. cùng một lúc return, đẩy giá trị của nó vào ngăn xếp đang gọi
  4. 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à

  1. phân bổ không gian ngăn xếp cho barqux
  2. thực hiện đệ quy câu lệnh đầu tiên và chuyển sang câu lệnh tiếp theo
    1. 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
    2. 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
  3. cùng một lúc return, đẩy giá trị của nó vào ngăn xếp đang gọi
  4. 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(). Để 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 fromhiện cả hai . Nó đình chỉ ngăn xếp và con trỏ hướng dẫn wrap chạy cofoo. Lưu ý rằng wrapvẫn bị treo cho đến khi cofookết thúc hoàn toàn. Bất cứ khi nào cofootạ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 fromcho 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 rootcoro_bkhô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ú ý, rootcó 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 asyncawait

Lời giải thích cho đến nay đã sử dụng rõ ràng từ vựng yieldyield fromtừ 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ớiasyncawaittồ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 forasync withlà 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 forwith.

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 awaitlẫ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 sleeps 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 awaitmộ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ì awaittừ 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 sleepbằng cách nhập awaitsự 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.sendchạy chương trình đăng quang cho đến khi yieldkết quả.

coroutine = asleep(100)
while True:
    print(coroutine.send(None))
    time.sleep(0.1)

Điều này cho chúng ta hai AsyncSleepsự kiện và sau đó là StopIterationthời điểm quy trình đăng quang được thực hiện. Lưu ý rằng độ trễ duy nhất là từ time.sleeptrong vòng lặp! Mỗi AsyncSleepchỉ 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:

  1. sắp xếp các thói quen theo thời gian thức dậy mong muốn của họ
  2. chọn cái đầu tiên muốn thức dậy
  3. đợi cho đến thời điểm này
  4. chạy quy trình điều tra này
  5. 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 listcho 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ự AsyncSleepkiện và runvò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ợ sleepphù 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 selectgọ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 openmộ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ề, writeablechứ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ư AsyncSleepyêu cầu, chúng ta cần xác định một sự kiện cho I / O. Với selectlogic 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 opentệ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 AsyncSleepchú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 returntô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]

select.selectcó 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, AsyncReadruntriể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 selectgọi luôn trả về cho tệp và cả hai openreadcó 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ư aiofilessử 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.recvthay 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à AsyncReadAsyncRecvlà 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 readas a recvfor 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


Sử dụng yield selftrong AsyncSleep cho tôi Task got back yieldlỗi, tại sao vậy? Tôi thấy rằng mã trong asyncio.Futures sử dụng điều đó. Sử dụng năng suất trần hoạt động tốt.
Ron Serruya

1
Vòng lặp sự kiện thường chỉ mong đợi các sự kiện của riêng họ. Bạn thường không thể kết hợp các sự kiện và vòng lặp sự kiện giữa các thư viện; các sự kiện hiển thị ở đây chỉ hoạt động với vòng lặp sự kiện được hiển thị. Cụ thể, asyncio chỉ sử dụng Không (tức là lợi suất trần) làm tín hiệu cho vòng lặp sự kiện. Sự kiện tương tác trực tiếp với đối tượng vòng lặp sự kiện để đăng ký đánh thức.
MisterMiyagi

12

Việc corogỡ rối của bạn về mặt khái niệm là đúng, nhưng hơi không đầy đủ.

awaitkhông tạm dừng vô điều kiện, nhưng chỉ khi nó gặp phải cuộc gọi chặn. Làm thế nào nó biết rằng một cuộc gọi đang bị chặn? Điều này được quyết định bởi mã đang được chờ đợi. Ví dụ: một triển khai có thể chờ đợi của đọc socket có thể được gỡ bỏ thành:

def read(sock, n):
    # sock must be in non-blocking mode
    try:
        return sock.recv(n)
    except EWOULDBLOCK:
        event_loop.add_reader(sock.fileno, current_task())
        return SUSPEND

Trong asyncio thực, mã tương đương sửa đổi trạng thái của a Futurethay vì trả về các giá trị ma thuật, nhưng khái niệm thì giống nhau. Khi được điều chỉnh thích hợp với một đối tượng giống như trình tạo, mã trên có thể được chỉnh sửa await.

Về phía người gọi, khi quy trình đăng ký của bạn chứa:

data = await read(sock, 1024)

Nó biến thành một thứ gì đó gần với:

data = read(sock, 1024)
if data is SUSPEND:
    return SUSPEND
self.pos += 1
self.parts[self.pos](...)

Những người quen thuộc với máy phát điện có xu hướng mô tả ở trên về yield fromviệc tạm dừng tự động.

Chuỗi tạm ngưng tiếp tục đến hết vòng lặp sự kiện, thông báo rằng quy trình đăng ký bị tạm ngừng, xóa nó khỏi tập hợp có thể chạy được và tiếp tục thực hiện các quy trình đăng ký có thể chạy được, nếu có. Nếu không có quy trình đăng ký nào có thể chạy được, thì vòng lặp sẽ đợi select()cho đến khi một trong hai trình mô tả tệp mà quy trình đăng ký quan tâm trở nên sẵn sàng cho IO. (Vòng lặp sự kiện duy trì ánh xạ tệp-mô tả-đến-đăng-nhập thường xuyên.)

Trong ví dụ trên, khi select()cho vòng lặp sự kiện sockcó thể đọc được, nó sẽ thêm corolại vào tập hợp có thể chạy được, do đó, nó sẽ được tiếp tục từ điểm tạm dừng.

Nói cách khác:

  1. Mọi thứ diễn ra trong cùng một chuỗi theo mặc định.

  2. Vòng lặp sự kiện chịu trách nhiệm lập lịch trình đăng ký và đánh thức chúng khi bất kỳ thứ gì chúng đang chờ đợi (thường là cuộc gọi IO thường chặn hoặc hết thời gian chờ) trở nên sẵn sàng.

Để có cái nhìn sâu sắc về các vòng lặp sự kiện lái xe đăng quang, tôi giới thiệu bài nói chuyện này của Dave Beazley, nơi anh ấy trình diễn mã hóa một vòng sự kiện từ đầu trước khán giả trực tiếp.


Cảm ơn bạn, điều này gần giống với những gì tôi đang theo đuổi, nhưng điều này vẫn không giải thích được tại sao async.wait_for()không làm những gì nó được cho là ... Tại sao việc thêm một lệnh gọi lại vào vòng lặp sự kiện và nói với nó để xử lý bao nhiêu lệnh gọi lại nó cần, bao gồm cả lệnh gọi lại bạn vừa thêm? Sự thất vọng của tôi asynciomột phần là do khái niệm cơ bản rất đơn giản và, ví dụ: Emacs Lisp đã triển khai cho các lứa tuổi, mà không sử dụng từ thông dụng ... (tức là create-async-processaccept-process-output- và đây là tất cả những gì cần thiết ... (
tt

10
@wvxvw Tôi đã cố gắng hết sức để trả lời câu hỏi bạn đã đăng, càng nhiều càng tốt vì chỉ có đoạn cuối cùng chứa sáu câu hỏi. Và vì vậy chúng tôi tiếp tục - nó không phải là wait_for không làm những gì nó phải làm (nó làm, đó là một quy trình điều tra mà bạn phải chờ đợi), mà là kỳ vọng của bạn không phù hợp với những gì hệ thống được thiết kế và triển khai để làm. Tôi nghĩ rằng vấn đề của bạn có thể phù hợp với asyncio nếu vòng lặp sự kiện đang chạy trong một chuỗi riêng biệt, nhưng tôi không biết chi tiết về trường hợp sử dụng của bạn và thành thật mà nói, thái độ của bạn không giúp bạn vui lắm.
user4815162342

5
@wvxvw 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...- Không có gì ngăn bạn triển khai khái niệm đơn giản này mà không có từ thông dụng cho Python :) Tại sao bạn lại sử dụng asyncio xấu xí này? Thực hiện của riêng bạn từ đầu. Ví dụ, bạn có thể bắt đầu với việc tạo ra một async.wait_for()hàm của riêng bạn , nó thực hiện chính xác những gì nó phải làm.
Mikhail Gerasimov

1
@MikhailGerasimov bạn có vẻ nghĩ đó là một câu hỏi tu từ. Nhưng, tôi muốn xóa tan bí ẩn cho bạn. Ngôn ngữ được thiết kế để nói với người khác. Tôi không thể chọn cho người khác ngôn ngữ mà họ nói, ngay cả khi tôi tin rằng ngôn ngữ họ nói là rác, điều tốt nhất tôi có thể làm là cố gắng thuyết phục họ là như vậy. Nói cách khác, nếu tôi được tự do lựa chọn, tôi sẽ không bao giờ chọn Python để bắt đầu, hãy để một mình asyncio. Nhưng, về nguyên tắc, đó không phải là quyết định của tôi. Tôi buộc phải sử dụng ngôn ngữ rác thông qua en.wikipedia.org/wiki/Ultimatum_game .
wvxvw

4

Tất cả chỉ tập trung vào hai thách thức chính mà asyncio đang giải quyết:

  • Làm thế nào để thực hiện nhiều I / O trong một luồng đơn lẻ?
  • Làm thế nào để thực hiện đa nhiệm hợp tác?

Câu trả lời cho điểm đầu tiên đã có từ lâu và được gọi là vòng lặp chọn . Trong python, nó được triển khai trong mô-đun bộ chọn .

Câu hỏi thứ hai liên quan đến khái niệm về coroutine , tức là các hàm có thể ngừng thực thi và được khôi phục sau này. Trong python, coroutines được thực hiện bằng cách sử dụng trình tạonăng suất từ câu lệnh. Đó là những gì ẩn đằng sau cú pháp async / await .

Thêm tài nguyên trong câu trả lời này .


CHỈNH SỬA: Giải quyết nhận xét của bạn về goroutines:

Tương đương gần nhất với goroutine trong asyncio thực sự không phải là coroutine mà là một task (xem sự khác biệt trong tài liệu ). Trong python, một coroutine (hoặc một trình tạo) không biết gì về các khái niệm về vòng lặp sự kiện hoặc I / O. Nó chỉ đơn giản là một hàm có thể ngừng thực thi bằng cách sử dụng yieldtrong khi vẫn giữ trạng thái hiện tại, vì vậy nó có thể được khôi phục sau này. Cácyield from cú pháp cho phép chaining họ một cách minh bạch.

Bây giờ, trong một nhiệm vụ asyncio, quy trình điều tra ở cuối chuỗi luôn kết thúc mang lại một tương lai . Tương lai này sau đó bong bóng theo vòng lặp sự kiện và được tích hợp vào máy móc bên trong. Khi tương lai được đặt thành thực hiện bởi một số gọi lại bên trong khác, vòng lặp sự kiện có thể khôi phục tác vụ bằng cách gửi tương lai trở lại chuỗi đăng quang.


CHỈNH SỬA: Giải quyết một số câu hỏi trong bài đăng của bạn:

I / O thực sự xảy ra như thế nào trong trường hợp này? Trong một chủ đề riêng biệt? Toàn bộ thông dịch viên có bị tạm ngưng và I / O xảy ra bên ngoài thông dịch viên không?

Không, không có gì xảy ra trong một chuỗi. I / O luôn được quản lý bởi vòng lặp sự kiện, chủ yếu thông qua các bộ mô tả tệp. Tuy nhiên, việc đăng ký các trình mô tả tệp đó thường bị ẩn bởi các coroutines cấp cao, khiến bạn trở nên khó khăn.

Chính xác thì I / O có nghĩa là gì? Nếu thủ tục python của tôi được gọi là thủ tục C open () và nó lần lượt gửi ngắt đến hạt nhân, từ bỏ quyền kiểm soát đối với nó, thì làm cách nào trình thông dịch Python biết về điều này và có thể tiếp tục chạy một số mã khác, trong khi mã hạt nhân thực hiện I / O và cho đến khi nó đánh thức thủ tục Python đã gửi ngắt ban đầu? Làm thế nào để trình thông dịch Python về nguyên tắc, nhận thức được điều này đang xảy ra?

I / O là bất kỳ cuộc gọi chặn nào. Trong asyncio, tất cả các hoạt động I / O phải đi qua vòng lặp sự kiện, vì như bạn đã nói, vòng lặp sự kiện không có cách nào để biết rằng một lệnh gọi chặn đang được thực hiện trong một số mã đồng bộ. Điều đó có nghĩa là bạn không được sử dụng đồng bộ opentrong ngữ cảnh của quy trình đăng ký. Thay vào đó, hãy sử dụng một thư viện chuyên dụng aiofiles cung cấp phiên bản không đồng bộ của open.


Nói rằng các coroutines được thực hiện bằng cách sử dụng yield fromkhông thực sự nói lên được điều gì. yield fromchỉ là một cấu trúc cú pháp, nó không phải là một khối xây dựng cơ bản mà máy tính có thể thực thi. Tương tự, đối với vòng lặp chọn. Có, các quy trình đăng nhập trong Go cũng sử dụng vòng lặp chọn, nhưng những gì tôi đang cố gắng làm sẽ hoạt động trong Go, nhưng không hoạt động trong Python. Tôi cần câu trả lời chi tiết hơn để hiểu tại sao nó không hoạt động.
wvxvw 27/02/18

Xin lỗi ... không, không hẳn. "tương lai", "nhiệm vụ", "cách minh bạch", "lợi nhuận từ" chỉ là các từ thông dụng, chúng không phải là đối tượng từ miền lập trình. lập trình có các biến, thủ tục và cấu trúc. Vì vậy, để nói rằng "goroutine là một nhiệm vụ" chỉ là một tuyên bố vòng vo đặt ra một câu hỏi. Cuối cùng, lời giải thích về những gì asyncio, đối với tôi, sẽ tóm tắt thành mã C minh họa những gì cú pháp Python được dịch sang.
wvxvw 27/02/18

Để giải thích thêm tại sao câu trả lời của bạn không trả lời câu hỏi của tôi: với tất cả thông tin bạn cung cấp, tôi không biết tại sao nỗ lực của tôi từ mã mà tôi đã đăng trong câu hỏi được liên kết không hoạt động. Tôi hoàn toàn chắc chắn rằng tôi có thể viết vòng lặp sự kiện theo cách mà mã này sẽ hoạt động. Trên thực tế, đây sẽ là cách tôi viết một vòng lặp sự kiện, nếu tôi phải viết một vòng lặp.
wvxvw 27/02/18

7
@wvxvw Tôi không đồng ý. Đó không phải là "buzzwords" mà là những khái niệm cấp cao đã được triển khai trong nhiều thư viện. Ví dụ, một tác vụ asyncio, một gevent greenlet và một goroutine đều tương ứng với cùng một thứ: một đơn vị thực thi có thể chạy đồng thời trong một luồng duy nhất. Ngoài ra, tôi không nghĩ rằng C là cần thiết để hiểu asyncio chút nào, trừ khi bạn muốn đi sâu vào hoạt động bên trong của trình tạo python.
Vincent

@wvxvw Xem chỉnh sửa thứ hai của tôi. Điều này sẽ xóa bỏ một số quan niệm sai lầm.
Vincent
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.