Sự khác biệt giữa coroutine và future / task trong Python 3.5?


99

Giả sử chúng ta có một hàm giả:

async def foo(arg):
    result = await some_remote_call(arg)
    return result.upper()

Sự khác biệt giữa:

coros = []
for i in range(5):
    coros.append(foo(i))

loop = get_event_loop()
loop.run_until_complete(wait(coros))

Và:

from asyncio import ensure_future

futures = []
for i in range(5):
    futures.append(ensure_future(foo(i)))

loop = get_event_loop()
loop.run_until_complete(wait(futures))

Lưu ý : Ví dụ trả về một kết quả, nhưng đây không phải là trọng tâm của câu hỏi. Khi giá trị trả về quan trọng, hãy sử dụng gather()thay vì wait().

Bất kể giá trị trả lại, tôi đang tìm kiếm sự rõ ràng ensure_future(). wait(coros)wait(futures)cả hai đều chạy các coroutines, vậy khi nào và tại sao nên gói coroutine ensure_future?

Về cơ bản, Cách đúng đắn (tm) để chạy một loạt các hoạt động không chặn bằng Python 3.5 là asyncgì?

Để có thêm tín dụng, điều gì sẽ xảy ra nếu tôi muốn gọi hàng loạt? Ví dụ: tôi cần gọi some_remote_call(...)1000 lần, nhưng tôi không muốn nghiền nát máy chủ web / cơ sở dữ liệu / vv với 1000 kết nối đồng thời. Điều này có thể làm được với một luồng hoặc nhóm quy trình, nhưng có cách nào để làm điều này với asynciokhông?

Câu trả lời:


94

Coroutine là một hàm tạo có thể vừa mang lại giá trị vừa chấp nhận các giá trị từ bên ngoài. Lợi ích của việc sử dụng quy trình đăng ký là chúng ta có thể tạm dừng việc thực thi một chức năng và tiếp tục lại sau. Trong trường hợp hoạt động mạng, bạn nên tạm dừng việc thực thi một chức năng trong khi chúng tôi đang chờ phản hồi. Chúng ta có thể sử dụng thời gian để chạy một số chức năng khác.

Tương lai giống như các Promiseđối tượng từ Javascript. Nó giống như một trình giữ chỗ cho một giá trị sẽ được hiện thực hóa trong tương lai. Trong trường hợp nêu trên, trong khi chờ I / O mạng, một hàm có thể cung cấp cho chúng ta một vùng chứa, một lời hứa rằng nó sẽ lấp đầy giá trị cho vùng chứa khi hoạt động hoàn tất. Chúng tôi giữ đối tượng tương lai và khi nó được hoàn thành, chúng tôi có thể gọi một phương thức trên đó để lấy kết quả thực tế.

Trả lời trực tiếp: Bạn không cần ensure_futurenếu bạn không cần kết quả. Chúng tốt nếu bạn cần kết quả hoặc truy xuất các trường hợp ngoại lệ xảy ra.

Tín dụng bổ sung: Tôi sẽ chọn run_in_executorvà chuyển một Executorphiên bản để kiểm soát số lượng công nhân tối đa.

Giải thích và Mã mẫu

Trong ví dụ đầu tiên, bạn đang sử dụng coroutines. Các waitchức năng phải mất một loạt các coroutines và kết hợp chúng lại với nhau. Vì vậy, wait()kết thúc khi tất cả các coroutines đã hết (hoàn thành / kết thúc trả về tất cả các giá trị).

loop = get_event_loop() # 
loop.run_until_complete(wait(coros))

Các run_until_completephương pháp sẽ đảm bảo rằng các vòng lặp là còn sống cho đến khi thực hiện xong. Vui lòng lưu ý cách bạn không nhận được kết quả của việc thực thi không đồng bộ trong trường hợp này.

Trong ví dụ thứ hai, bạn đang sử dụng ensure_futurehàm để bọc một quy trình đăng quang và trả về một Taskđối tượng là một loại Future. Quy trình đăng quang được lên lịch thực hiện trong vòng lặp sự kiện chính khi bạn gọi ensure_future. Đối tượng tương lai / nhiệm vụ được trả về chưa có giá trị nhưng theo thời gian, khi các hoạt động mạng kết thúc, đối tượng tương lai sẽ giữ kết quả của hoạt động.

from asyncio import ensure_future

futures = []
for i in range(5):
    futures.append(ensure_future(foo(i)))

loop = get_event_loop()
loop.run_until_complete(wait(futures))

Vì vậy, trong ví dụ này, chúng tôi đang làm điều tương tự ngoại trừ chúng tôi đang sử dụng tương lai thay vì chỉ sử dụng coroutines.

Hãy xem một ví dụ về cách sử dụng asyncio / coroutines / futures:

import asyncio


async def slow_operation():
    await asyncio.sleep(1)
    return 'Future is done!'


def got_result(future):
    print(future.result())

    # We have result, so let's stop
    loop.stop()


loop = asyncio.get_event_loop()
task = loop.create_task(slow_operation())
task.add_done_callback(got_result)

# We run forever
loop.run_forever()

Ở đây, chúng tôi đã sử dụng create_taskphương thức trên loopđối tượng. ensure_futuresẽ lên lịch nhiệm vụ trong vòng lặp sự kiện chính. Phương pháp này cho phép chúng tôi lập lịch trình đăng ký trên một vòng lặp mà chúng tôi chọn.

Chúng ta cũng thấy khái niệm thêm một cuộc gọi lại bằng cách sử dụng add_done_callbackphương thức trên đối tượng tác vụ.

A Taskdonekhi chương trình đăng quang trả về một giá trị, tăng một ngoại lệ hoặc bị hủy bỏ. Có những phương pháp để kiểm tra những sự cố này.

Tôi đã viết một số bài đăng trên blog về các chủ đề này có thể giúp ích:

Tất nhiên, bạn có thể tìm thêm chi tiết trên hướng dẫn chính thức: https://docs.python.org/3/library/asyncio.html


3
Tôi đã cập nhật câu hỏi của mình để rõ ràng hơn một chút - nếu tôi không cần kết quả từ quy trình đăng quang, tôi có cần sử dụng ensure_future()không? Và nếu tôi cần kết quả, tôi có thể sử dụng run_until_complete(gather(coros))không?
đan xen

1
ensure_futurelập lịch trình đăng quang được thực thi trong vòng lặp sự kiện. Vì vậy, tôi sẽ nói có, nó bắt buộc. Nhưng tất nhiên, bạn cũng có thể lên lịch cho các coroutines bằng các hàm / phương thức khác. Có, bạn có thể sử dụng gather()- nhưng tập hợp sẽ đợi cho đến khi tất cả các câu trả lời được thu thập.
masnun

5
@AbuAshrafMasnun @knite gatherwaitthực sự bọc các coroutines đã cho làm các tác vụ sử dụng ensure_future(xem các nguồn tại đâytại đây ). Vì vậy, không có ích gì khi sử dụng ensure_futuretrước, và nó không liên quan gì đến việc nhận được kết quả hay không.
Vincent

8
@AbuAshrafMasnun @knite Ngoài ra, ensure_futurecó một loopđối số, vì vậy không có lý do gì để sử dụng loop.create_taskquá ensure_future. Và run_in_executorsẽ không hoạt động với coroutines, một semaphore nên được sử dụng thay thế.
Vincent

2
@vincent có lý do để sử dụng create_taskhơn ensure_future, xem tài liệu . Tríchcreate_task() (added in Python 3.7) is the preferable way for spawning new tasks.
Masi

24

Câu trả lời đơn giản

  • Việc gọi một hàm coroutine ( async def) KHÔNG chạy nó. Nó trả về một đối tượng coroutine, như hàm máy phát trả về các đối tượng máy tạo.
  • await lấy giá trị từ coroutines, tức là "gọi" coroutine
  • eusure_future/create_task lên lịch trình đăng quang để chạy trên vòng lặp sự kiện vào lần lặp tiếp theo (mặc dù không đợi chúng kết thúc, giống như một chuỗi daemon).

Một số ví dụ về mã

Trước tiên, hãy xóa một số thuật ngữ:

  • chức năng đăng quang, một trong những bạn async defs;
  • đối tượng coroutine, những gì bạn nhận được khi "gọi" một hàm coroutine;
  • nhiệm vụ, một đối tượng được bao bọc xung quanh một đối tượng coroutine để chạy trên vòng lặp sự kiện.

Trường hợp 1, awaittheo quy trình khám nghiệm

Chúng tôi tạo hai coroutines, awaitmột và sử dụng create_taskđể chạy cái kia.

import asyncio
import time

# coroutine function
async def p(word):
    print(f'{time.time()} - {word}')


async def main():
    loop = asyncio.get_event_loop()
    coro = p('await')  # coroutine
    task2 = loop.create_task(p('create_task'))  # <- runs in next iteration
    await coro  # <-- run directly
    await task2

if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

bạn sẽ nhận được kết quả:

1539486251.7055213 - await
1539486251.7055705 - create_task

Giải thích:

task1 được thực thi trực tiếp và task2 được thực thi trong lần lặp sau.

Trường hợp 2, nhường quyền kiểm soát cho vòng lặp sự kiện

Nếu chúng ta thay thế hàm main, chúng ta có thể thấy một kết quả khác:

async def main():
    loop = asyncio.get_event_loop()
    coro = p('await')
    task2 = loop.create_task(p('create_task'))  # scheduled to next iteration
    await asyncio.sleep(1)  # loop got control, and runs task2
    await coro  # run coro
    await task2

bạn sẽ nhận được kết quả:

-> % python coro.py
1539486378.5244057 - create_task
1539486379.5252144 - await  # note the delay

Giải thích:

Khi gọi asyncio.sleep(1), điều khiển được trả lại cho vòng lặp sự kiện và vòng lặp kiểm tra các tác vụ để chạy, sau đó nó chạy tác vụ được tạo bởi create_task.

Lưu ý rằng, trước tiên chúng ta gọi hàm coroutine, nhưng không gọi awaitnó, vì vậy chúng ta chỉ tạo một hàm coroutine duy nhất và không làm cho nó chạy. Sau đó, chúng ta gọi lại hàm coroutine và kết thúc nó trong một create_taskcuộc gọi, create_task sẽ thực sự lên lịch cho hàm coroutine chạy trong lần lặp tiếp theo. Vì vậy, trong kết quả, create taskđược thực thi trước đó await.

Trên thực tế, vấn đề ở đây là trao lại quyền kiểm soát cho vòng lặp, bạn có thể sử dụng asyncio.sleep(0)để xem kết quả tương tự.

Dưới mui xe

loop.create_taskthực sự gọi asyncio.tasks.Task(), sẽ gọi loop.call_soon. Và loop.call_soonsẽ đưa nhiệm vụ vào loop._ready. Trong mỗi lần lặp lại của vòng lặp, nó sẽ kiểm tra mọi lệnh gọi lại trong loop._ready và chạy nó.

asyncio.wait, asyncio.ensure_futureasyncio.gatherthực sự gọi loop.create_tasktrực tiếp hoặc gián tiếp.

Cũng lưu ý trong tài liệu :

Các cuộc gọi lại được gọi theo thứ tự mà chúng được đăng ký. Mỗi lần gọi lại sẽ được gọi chính xác một lần.


1
Cảm ơn vì một lời giải thích rõ ràng! Phải nói, đó là một thiết kế khá khủng khiếp. API cấp cao đang rò rỉ phần trừu tượng cấp thấp, làm phức tạp API.
Boris Burkov

1
kiểm tra dự án curio, được thiết kế tốt
ospider 20/02 '19

Lời giải thích hay! Tôi nghĩ rằng ảnh hưởng của await task2cuộc gọi có thể được làm rõ. Trong cả hai ví dụ, lời gọi loop.create_task () là thứ lên lịch task2 trên vòng lặp sự kiện. Vì vậy, trong cả hai ex, bạn có thể xóa await task2task2 và cuối cùng vẫn sẽ chạy. Trong ex2, hành vi sẽ giống hệt nhau, vì await task2tôi tin là chỉ lên lịch cho nhiệm vụ đã hoàn thành (sẽ không chạy lần thứ hai), trong khi ở ex1, hành vi sẽ hơi khác vì task2 sẽ không được thực thi cho đến khi main hoàn thành. Để thấy sự khác biệt, hãy thêm print("end of main")vào cuối phần chính của ex1
Andrew

10

Một nhận xét của Vincent được liên kết với https://github.com/python/asyncio/blob/master/asyncio/tasks.py#L346 , cho thấy rằng wait()kết thúc các quy trình ensure_future()cho bạn!

Nói cách khác, chúng ta cần một tương lai, và các quy trình sẽ được âm thầm chuyển thành chúng.

Tôi sẽ cập nhật câu trả lời này khi tôi tìm thấy lời giải thích rõ ràng về cách thực hiện hàng loạt các quy trình / tương lai.


Nó có nghĩa là đối với một đối tượng coroutine c, await ctương đương với await create_task(c)?
Alexey

3

Từ BDFL [2013]

Nhiệm vụ

  • Đó là một quy trình đăng quang được bao bọc trong một Tương lai
  • lớp Nhiệm vụ là một lớp con của lớp Tương lai
  • Vì vậy, nó cũng hoạt động với await !

  • Nó khác với lễ đăng quang trần như thế nào?
  • Nó có thể tiến bộ mà không cần chờ đợi
    • Miễn là bạn chờ đợi một cái gì đó khác, tức là
      • chờ đợi [something_else]

Với suy nghĩ này, nó ensure_futurecó ý nghĩa như một cái tên để tạo Nhiệm vụ vì kết quả của Tương lai sẽ được tính cho dù bạn có chờ đợi nó hay không (miễn là bạn đang chờ đợi điều gì đó). Điều này cho phép vòng lặp sự kiện hoàn thành Nhiệm vụ của bạn trong khi bạn đang chờ đợi những việc khác. Lưu ý rằng trong Python 3.7 create_tasklà cách ưa thích để đảm bảo một tương lai .

Lưu ý: Tôi đã thay đổi "sản lượng từ" trong các trang trình bày của Guido thành "chờ đợi" ở đây cho hiện đại.

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.