ความแตกต่างระหว่าง Coroutine และ Future / Task ใน Python 3.5


102

สมมติว่าเรามีฟังก์ชันดัมมี่:

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

อะไรคือความแตกต่างระหว่าง:

import asyncio    

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

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

และ:

import asyncio

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

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

บันทึก : ตัวอย่างส่งคืนผลลัพธ์ แต่นี่ไม่ใช่จุดสำคัญของคำถาม เมื่อค่าตอบแทนเรื่องการใช้งานแทนgather()wait()

โดยไม่คำนึงถึงค่าตอบแทน, ensure_future()ฉันกำลังมองหาเพื่อความชัดเจนในการwait(coros)และwait(futures)ทั้งสองเรียกใช้โครูทีนดังนั้นเมื่อใดและทำไมจึงควรพันโครูทีนensure_future?

โดยพื้นฐานแล้ววิธีที่ถูกต้อง (tm) ในการเรียกใช้การดำเนินการที่ไม่บล็อกโดยใช้ Python 3.5 คือasyncอะไร?

หากต้องการรับเครดิตพิเศษจะต้องทำอย่างไรหากต้องการโทรเป็นกลุ่ม เช่นต้องโทรsome_remote_call(...) 1,000 ครั้ง แต่ฉันไม่ต้องการทำลายเว็บเซิร์ฟเวอร์ / ฐานข้อมูล / ฯลฯ ด้วยการเชื่อมต่อพร้อมกัน 1,000 ครั้ง สิ่งนี้ทำได้ด้วยเธรดหรือพูลกระบวนการ แต่มีวิธีดำเนินการด้วยasyncioหรือไม่?

การอัปเดตปี 2020 (Python 3.7+) : อย่าใช้ข้อมูลโค้ดเหล่านี้ ใช้แทน:

import asyncio

async def do_something_async():
    tasks = []
    for i in range(5):
        tasks.append(asyncio.create_task(foo(i)))
    await asyncio.gather(*tasks)

def do_something():
    asyncio.run(do_something_async)

ลองพิจารณาใช้Trioซึ่งเป็นทางเลือกของบุคคลที่สามที่มีประสิทธิภาพแทน asyncio

คำตอบ:


97

โครูทีนเป็นฟังก์ชันเครื่องกำเนิดไฟฟ้าที่สามารถให้ทั้งค่าและยอมรับค่าจากภายนอก ประโยชน์ของการใช้โครูทีนคือเราสามารถหยุดการทำงานของฟังก์ชันชั่วคราวและดำเนินการต่อได้ในภายหลัง ในกรณีของการทำงานของเครือข่ายคุณควรหยุดการทำงานของฟังก์ชันชั่วคราวในขณะที่เรากำลังรอการตอบกลับ เราสามารถใช้เวลาในการเรียกใช้ฟังก์ชันอื่น ๆ

อนาคตก็เหมือน Promiseวัตถุจาก Javascript เป็นเหมือนตัวยึดสำหรับมูลค่าที่จะเกิดขึ้นในอนาคต ในกรณีที่กล่าวมาข้างต้นในขณะที่รอบนเครือข่าย I / O ฟังก์ชันสามารถให้คอนเทนเนอร์แก่เราโดยสัญญาว่าจะเติมค่าให้กับคอนเทนเนอร์เมื่อการดำเนินการเสร็จสิ้น เรายึดมั่นในวัตถุในอนาคตและเมื่อมันสำเร็จเราสามารถเรียกเมธอดบนวัตถุนั้นเพื่อดึงผลลัพธ์ที่แท้จริง

คำตอบโดยตรง:คุณไม่ต้องการensure_futureถ้าคุณไม่ต้องการผลลัพธ์ เป็นสิ่งที่ดีหากคุณต้องการผลลัพธ์หรือดึงข้อยกเว้นที่เกิดขึ้น

เครดิตพิเศษ:ฉันจะเลือกrun_in_executorและส่งผ่านExecutorอินสแตนซ์เพื่อควบคุมจำนวนคนงานสูงสุด

คำอธิบายและรหัสตัวอย่าง

ในตัวอย่างแรกคุณกำลังใช้โครูทีน waitฟังก์ชั่นใช้เวลาพวงของ coroutines และรวมเข้าด้วยกัน ดังนั้นจะwait()เสร็จสิ้นเมื่อโครูทีนทั้งหมดหมดลง (เสร็จสมบูรณ์ / เสร็จสิ้นการคืนค่าทั้งหมด)

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

run_until_completeวิธีการที่จะทำให้แน่ใจว่าวงยังมีชีวิตอยู่จนกว่าจะดำเนินการเสร็จสิ้น โปรดสังเกตว่าคุณไม่ได้รับผลลัพธ์ของการดำเนินการ async ในกรณีนี้อย่างไร

ในตัวอย่างที่สองคุณกำลังใช้ensure_futureฟังก์ชันเพื่อตัดโครูทีนและส่งคืนTaskอ็อบเจ็กต์ซึ่งเป็นชนิดของFuture. coroutine ensure_futureมีกำหนดที่จะดำเนินการในห่วงเหตุการณ์สำคัญเมื่อคุณเรียก วัตถุในอนาคต / งานที่ส่งคืนยังไม่มีค่า แต่เมื่อเวลาผ่านไปเมื่อการทำงานของเครือข่ายเสร็จสิ้นวัตถุในอนาคตจะเก็บผลลัพธ์ของการดำเนินการไว้

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

ดังนั้นในตัวอย่างนี้เรากำลังทำสิ่งเดียวกันยกเว้นว่าเราใช้ฟิวเจอร์สแทนที่จะใช้โครูทีน

มาดูตัวอย่างวิธีการใช้ 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()

ที่นี่เราได้ใช้create_taskวิธีการกับloopวัตถุ ensure_futureจะกำหนดเวลางานในลูปเหตุการณ์หลัก วิธีนี้ช่วยให้เรากำหนดเวลาโครูทีนในลูปที่เราเลือกได้

นอกจากนี้เรายังเห็นแนวคิดของการเพิ่มการเรียกกลับโดยใช้add_done_callbackวิธีการบนวัตถุงาน

A Taskคือdoneเมื่อโครูทีนส่งคืนค่ายกข้อยกเว้นหรือถูกยกเลิก มีวิธีการตรวจสอบเหตุการณ์เหล่านี้

ฉันได้เขียนบล็อกโพสต์ในหัวข้อเหล่านี้ซึ่งอาจช่วยได้:

แน่นอนคุณสามารถดูรายละเอียดเพิ่มเติมได้จากคู่มืออย่างเป็นทางการ: https://docs.python.org/3/library/asyncio.html


3
ฉันได้อัปเดตคำถามของฉันให้ชัดเจนยิ่งขึ้น - ถ้าฉันไม่ต้องการผลลัพธ์จากโครูทีนฉันยังต้องใช้อยู่ensure_future()หรือไม่? และถ้าฉันต้องการผลลัพธ์ฉันจะใช้run_until_complete(gather(coros))ไม่ได้หรือ?
ถัก

1
ensure_futureกำหนดเวลาให้โครูทีนดำเนินการในลูปเหตุการณ์ ดังนั้นฉันจะตอบว่าใช่มันจำเป็น แต่แน่นอนคุณสามารถกำหนดเวลาโครูทีนโดยใช้ฟังก์ชัน / วิธีการอื่น ๆ ได้เช่นกัน ใช่คุณสามารถใช้ได้gather()- แต่การรวบรวมจะรอจนกว่าจะรวบรวมคำตอบทั้งหมด
มัสนัน

5
@AbuAshrafMasnun @knite gatherและwaitห่อโครูทีนที่กำหนดให้เป็นงานโดยใช้ensure_future(ดูแหล่งที่มาที่นี่และที่นี่ ) ดังนั้นจึงไม่มีจุดที่จะใช้ensure_futureล่วงหน้าและไม่มีส่วนเกี่ยวข้องกับการได้รับผลลัพธ์หรือไม่
Vincent

8
@AbuAshrafMasnun @knite นอกจากนี้ยังensure_futureมีloopข้อโต้แย้งจึงมีเหตุผลที่จะใช้ไม่มีมากกว่าloop.create_task ensure_futureและrun_in_executorจะไม่ทำงานกับ coroutines เป็นสัญญาณที่ควรจะนำมาใช้แทน
Vincent

2
@vincent มีเหตุผลที่จะใช้create_taskมากกว่าensure_futureดูเอกสาร อ้างcreate_task() (added in Python 3.7) is the preferable way for spawning new tasks.
masi

24

คำตอบง่ายๆ

  • การเรียกใช้ฟังก์ชัน coroutine ( async def) จะไม่เรียกใช้ มันส่งคืนอ็อบเจ็กต์ coroutine เช่นฟังก์ชัน generator จะส่งคืนอ็อบเจ็กต์ตัวสร้าง
  • await ดึงค่าจากโครูทีนคือ "เรียก" โครูทีน
  • eusure_future/create_task กำหนดเวลาให้โครูทีนรันบนลูปเหตุการณ์ในการวนซ้ำครั้งถัดไป (แม้ว่าจะไม่รอให้เสร็จสิ้นเช่นเดมอนเธรด)

ตัวอย่างโค้ดบางส่วน

ก่อนอื่นเรามาล้างคำศัพท์บางคำ:

  • ฟังก์ชั่นโครูทีนคุณ async def ;
  • วัตถุโครูทีนสิ่งที่คุณได้รับเมื่อคุณ "เรียก" ฟังก์ชันโครูทีน
  • งานวัตถุที่ห่อหุ้มรอบวัตถุโครูทีนเพื่อรันบนลูปเหตุการณ์

กรณีที่ 1 awaitบนโครูทีน

เราสร้างโครูทีนสองอันawaitอันหนึ่งและใช้create_taskเพื่อรันอีกอันหนึ่ง

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

คุณจะได้รับผล:

1539486251.7055213 - await
1539486251.7055705 - create_task

อธิบาย:

task1 ถูกดำเนินการโดยตรงและ task2 ถูกดำเนินการในการทำซ้ำต่อไปนี้

กรณีที่ 2 ให้การควบคุมลูปเหตุการณ์

หากเราแทนที่ฟังก์ชันหลักเราจะเห็นผลลัพธ์ที่แตกต่างกัน:

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

คุณจะได้รับผล:

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

อธิบาย:

เมื่อโทรasyncio.sleep(1), create_taskควบคุมได้รับการยอมแพ้กลับไปห่วงเหตุการณ์และตรวจสอบห่วงสำหรับงานที่จะวิ่งแล้วก็วิ่งงานที่สร้างขึ้นโดย

โปรดทราบว่าก่อนอื่นเราเรียกใช้ฟังก์ชันโครูทีน แต่ไม่ใช่ฟังก์ชันawaitนี้ดังนั้นเราจึงสร้างโครูทีนเพียงตัวเดียวและไม่ทำให้มันทำงาน จากนั้นเราเรียกฟังก์ชันโครูทีนอีกครั้งและรวมไว้ในการcreate_taskโทร creat_task จะกำหนดเวลาให้โครูทีนทำงานในการวนซ้ำครั้งถัดไป ดังนั้นผลลัพธ์create taskจะถูกดำเนินการก่อนawaitจะถูกดำเนินการก่อน

จริงๆแล้วประเด็นตรงนี้คือการให้การควบคุมย้อนกลับไปที่ลูปคุณสามารถใช้asyncio.sleep(0)เพื่อดูผลลัพธ์เดียวกันได้

ใต้ฝากระโปรง

loop.create_taskโทรจริงasyncio.tasks.Task()ซึ่งจะโทรloop.call_soon. และloop.call_soonจะนำงานเข้าloop._readyมา ในระหว่างการวนซ้ำแต่ละครั้งจะตรวจสอบการเรียกกลับทุกครั้งในลูป _ พร้อมและเรียกใช้

asyncio.wait, asyncio.ensure_futureและasyncio.gatherเรียกจริงloop.create_taskโดยตรงหรือโดยอ้อม

นอกจากนี้โปรดทราบในเอกสาร :

การโทรกลับจะถูกเรียกตามลำดับที่มีการลงทะเบียน การโทรกลับแต่ละครั้งจะถูกเรียกเพียงครั้งเดียว


1
ขอบคุณสำหรับคำอธิบายที่ชัดเจน! ต้องบอกว่ามันเป็นการออกแบบที่แย่มาก API ระดับสูงกำลังรั่วไหลนามธรรมระดับต่ำซึ่งทำให้ API มีความซับซ้อนมากเกินไป
Boris Burkov

1
ตรวจสอบโครงการ curio ซึ่งได้รับการออกแบบมาเป็นอย่างดี
ospider

คำอธิบายที่ดี! ฉันคิดว่าผลของการawait task2โทรสามารถชี้แจงได้ ในทั้งสองตัวอย่างการเรียก loop.create_task () คือสิ่งที่จัดกำหนดการ task2 บนลูปเหตุการณ์ ดังนั้นในทั้งสอง exs คุณสามารถลบawait task2และยังคง task2 จะทำงานในที่สุด ใน ex2 พฤติกรรมจะเหมือนกันเนื่องจากawait task2ฉันเชื่อว่าเป็นเพียงการจัดตารางงานที่เสร็จสมบูรณ์แล้ว (ซึ่งจะไม่ทำงานเป็นครั้งที่สอง) ในขณะที่ใน ex1 พฤติกรรมจะแตกต่างกันเล็กน้อยเนื่องจากจะไม่ดำเนินการ task2 จนกว่า main จะเสร็จสมบูรณ์ หากต้องการดูความแตกต่างให้เพิ่มprint("end of main")ที่ส่วนท้ายของ ex1 main
Andrew

11

ความคิดเห็นของ Vincent ที่เชื่อมโยงกับhttps://github.com/python/asyncio/blob/master/asyncio/tasks.py#L346ซึ่งแสดงให้เห็นว่าwait()ครอบคลุมเนื้อหาensure_future()สำหรับคุณ!

กล่าวอีกนัยหนึ่งเราต้องการอนาคตและโครูทีนจะถูกเปลี่ยนเป็นสิ่งเหล่านี้อย่างเงียบ ๆ

ฉันจะอัปเดตคำตอบนี้เมื่อฉันพบคำอธิบายที่ชัดเจนเกี่ยวกับวิธีการจัดชุด / ฟิวเจอร์ส


หมายความว่าสำหรับวัตถุโคcรูทีนawait cเทียบเท่ากับawait create_task(c)?
Alexey

3

จาก BDFL [2013]

งาน

  • มันคือโครูทีนที่ห่อหุ้มด้วยอนาคต
  • class Task เป็นคลาสย่อยของคลาส Future
  • ดังนั้นจึงทำงานร่วมกับรอคอยมากเกินไป!

  • แตกต่างจากโครูทีนเปล่าอย่างไร?
  • สามารถสร้างความก้าวหน้าได้โดยไม่ต้องรอ
    • ตราบเท่าที่คุณรออย่างอื่นเช่น
      • รอ [something_else]

ด้วยเหตุนี้ensure_futureจึงเหมาะสมกับชื่อในการสร้างงานเนื่องจากผลลัพธ์ของอนาคตจะถูกคำนวณไม่ว่าคุณจะรอหรือไม่(ตราบเท่าที่คุณรอบางสิ่ง) สิ่งนี้ช่วยให้การวนซ้ำของเหตุการณ์ทำงานของคุณให้เสร็จสมบูรณ์ในขณะที่คุณกำลังรอสิ่งอื่น ๆ โปรดทราบว่าในหลาม 3.7 create_taskเป็นวิธีที่ต้องการให้แน่ใจว่าในอนาคต

หมายเหตุ: ฉันเปลี่ยน "ผลตอบแทนจาก" ในสไลด์ของ Guido เป็น "รอ" ที่นี่เพื่อความทันสมัย

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.