asyncio ทำงานอย่างไร?


138

คำถามนี้ได้รับแรงบันดาลใจจากคำถามอื่นของฉัน: จะรอใน cdef ได้อย่างไร?

มีบทความและบล็อกโพสต์มากมายบนเว็บเกี่ยวกับasyncioแต่ทั้งหมดเป็นเพียงผิวเผิน ฉันไม่พบข้อมูลใด ๆ เกี่ยวกับวิธีasyncioการนำไปใช้จริงและสิ่งใดที่ทำให้ I / O ไม่ตรงกัน ฉันพยายามอ่านซอร์สโค้ด แต่เป็นรหัส C ระดับสูงสุดหลายพันบรรทัดซึ่งส่วนใหญ่เกี่ยวข้องกับอ็อบเจ็กต์เสริม แต่ที่สำคัญที่สุดคือยากที่จะเชื่อมต่อระหว่างไวยากรณ์ Python กับโค้ด C ที่จะแปล เป็น.

เอกสารของ Asycnio เองก็มีประโยชน์ไม่น้อย ไม่มีข้อมูลเกี่ยวกับวิธีการทำงานมีเพียงหลักเกณฑ์บางประการเกี่ยวกับวิธีใช้งานซึ่งบางครั้งอาจทำให้เข้าใจผิด / เขียนได้ไม่ดี

ฉันคุ้นเคยกับการใช้โครูทีนของ Go และหวังว่า Python จะทำแบบเดียวกัน หากเป็นเช่นนั้นรหัสที่ฉันสร้างขึ้นในโพสต์ที่ลิงก์ด้านบนจะใช้งานได้ เนื่องจากไม่เป็นเช่นนั้นตอนนี้ฉันกำลังพยายามหาสาเหตุ การคาดเดาที่ดีที่สุดของฉันมีดังนี้โปรดแก้ไขฉันที่ฉันผิด:

  1. ข้อกำหนดขั้นตอนของแบบฟอร์ม async def foo(): ...ถูกตีความว่าเป็นวิธีการของคลาสที่สืบทอดcoroutineมา
  2. บางที async defถูกแบ่งออกเป็นหลายวิธีโดยawaitคำสั่งโดยที่วัตถุซึ่งเรียกวิธีการเหล่านี้สามารถติดตามความคืบหน้าของการดำเนินการได้จนถึงตอนนี้
  3. หากข้างต้นเป็นจริงโดยพื้นฐานแล้วการเรียกใช้โครูทีนจะทำให้เมธอดเรียกใช้วัตถุโครูทีนโดยผู้จัดการระดับโลกบางคน (ลูป?)
  4. ผู้จัดการระดับโลกทราบว่าเมื่อใดที่การดำเนินการ I / O ดำเนินการโดยโค้ด Python (เท่านั้น?) และสามารถเลือกหนึ่งในวิธีการโครูทีนที่รอดำเนินการเพื่อดำเนินการหลังจากที่วิธีการดำเนินการปัจจุบันยกเลิกการควบคุม (กดที่ awaitคำสั่ง ).

กล่าวอีกนัยหนึ่งนี่คือความพยายามของฉันในการ "desugaring" ของasyncioไวยากรณ์บางส่วนไปสู่สิ่งที่เข้าใจได้ง่ายขึ้น

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

หากการคาดเดาของฉันพิสูจน์ได้ว่าถูกต้อง: แสดงว่าฉันมีปัญหา I / O เกิดขึ้นจริงในสถานการณ์นี้อย่างไร ในกระทู้แยกกัน? ล่ามทั้งหมดถูกระงับและ I / O เกิดขึ้นนอกล่ามหรือไม่ I / O หมายถึงอะไรกันแน่? หากโพรซีเดอร์ python ของฉันเรียกว่าโพรซีเดอร์ C open()และส่งการขัดจังหวะไปยังเคอร์เนลโดยยกเลิกการควบคุมมันล่าม Python รู้ได้อย่างไรเกี่ยวกับเรื่องนี้และสามารถรันโค้ดอื่น ๆ ต่อไปได้ในขณะที่โค้ดเคอร์เนลทำ I / O จริงและจนกว่า มันปลุกขั้นตอน Python ซึ่งส่งการขัดจังหวะมา แต่เดิม? โดยหลักการแล้วล่าม Python จะระวังเหตุการณ์นี้ได้อย่างไร?


2
ตรรกะส่วนใหญ่ถูกจัดการโดยการใช้งานวนรอบเหตุการณ์ ดูวิธีการBaseEventLoopใช้งานCPython : github.com/python/cpython/blob/…
Blender

@Blender ตกลงฉันคิดว่าในที่สุดฉันก็พบสิ่งที่ต้องการ แต่ตอนนี้ฉันไม่เข้าใจเหตุผลที่โค้ดเขียนแบบที่เป็นอยู่ เหตุใด_run_onceซึ่งเป็นฟังก์ชันที่มีประโยชน์เพียงอย่างเดียวในโมดูลทั้งหมดนี้จึงทำให้เป็น "ส่วนตัว" การนำไปใช้นั้นแย่มาก แต่ก็มีปัญหาน้อยกว่า เหตุใดฟังก์ชันเดียวที่คุณต้องการเรียกใช้ในวงเหตุการณ์จึงถูกทำเครื่องหมายว่า "อย่าโทรหาฉัน"
wvxvw

นั่นเป็นคำถามสำหรับรายชื่ออีเมล กรณีการใช้งานใดที่คุณต้องสัมผัส_run_onceตั้งแต่แรก?
Blender

8
นั่นไม่ได้ตอบคำถามของฉันจริงๆ คุณจะแก้ปัญหาที่เป็นประโยชน์ได้_run_onceอย่างไรโดยใช้เพียง? asyncioมีความซับซ้อนและมีข้อบกพร่อง แต่โปรดให้การอภิปรายเป็นไปอย่างราบรื่น อย่าดูถูกนักพัฒนาที่อยู่เบื้องหลังโค้ดที่คุณเองไม่เข้าใจ
Blender

1
@ user8371915 หากคุณเชื่อว่ามีสิ่งใดที่ฉันไม่ได้กล่าวถึงคุณสามารถเพิ่มหรือแสดงความคิดเห็นในคำตอบของฉันได้
Bharel

คำตอบ:


218

asyncio ทำงานอย่างไร?

ก่อนที่จะตอบคำถามนี้เราจำเป็นต้องเข้าใจคำศัพท์พื้นฐานบางคำให้ข้ามคำเหล่านี้ไปหากคุณรู้จักคำศัพท์เหล่านี้อยู่แล้ว

เครื่องกำเนิดไฟฟ้า

เครื่องกำเนิดไฟฟ้าเป็นวัตถุที่อนุญาตให้เราระงับการทำงานของฟังก์ชัน python ผู้ใช้ curated yieldเครื่องกำเนิดไฟฟ้าจะดำเนินการโดยใช้คำหลัก โดยการสร้างฟังก์ชันปกติที่มีyieldคำสำคัญเราเปลี่ยนฟังก์ชันนั้นให้เป็นตัวสร้าง:

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

อย่างที่คุณเห็นการเรียกnext()ใช้เครื่องกำเนิดไฟฟ้าทำให้ล่ามโหลดเฟรมของการทดสอบและส่งคืนyieldค่า ed เรียกnext()อีกครั้งทำให้เฟรมโหลดอีกครั้งในสแต็กล่ามและดำเนินการต่อในyieldค่าอื่น

เมื่อเรียกครั้งที่สามnext()เครื่องกำเนิดไฟฟ้าของเราเสร็จสิ้นและStopIterationถูกโยนทิ้ง

การสื่อสารกับเครื่องกำเนิดไฟฟ้า

คุณลักษณะน้อยที่รู้จักกันของเครื่องกำเนิดไฟฟ้าเป็นความจริงที่ว่าคุณสามารถสื่อสารกับพวกเขาใช้สองวิธี: และ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

เมื่อเรียกgen.send()ค่าจะถูกส่งเป็นค่าส่งคืนจากไฟล์yieldคีย์เวิร์ด

gen.throw() ในทางกลับกันอนุญาตให้โยนข้อยกเว้นภายในเครื่องกำเนิดไฟฟ้าโดยยกข้อยกเว้นที่จุดเดียวกัน yieldที่ถูกเรียกว่า

การคืนค่าจากเครื่องกำเนิดไฟฟ้า

การส่งคืนค่าจากเครื่องกำเนิดไฟฟ้าส่งผลให้ค่าถูกใส่ไว้ในStopIterationข้อยกเว้น เราสามารถกู้คืนค่าจากข้อยกเว้นและใช้ตามความต้องการของเราได้ในภายหลัง

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

นี่คือคำหลักใหม่: yield from

Python 3.4 มาพร้อมกับการเพิ่มคีย์เวิร์ดใหม่: yield from. สิ่งที่คำหลักที่ช่วยให้เราสามารถทำคือผ่านใด ๆnext(), send()และthrow()เป็นเครื่องกำเนิดไฟฟ้าชั้นซ้อนกันมากที่สุด หากตัวสร้างภายในส่งคืนค่าก็จะเป็นค่าส่งคืนของ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

ฉันเคยเขียนบทความเพื่ออธิบายเพิ่มเติมในหัวข้อนี้

วางมันทั้งหมดเข้าด้วยกัน

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

โครูทีนเป็นฟังก์ชันที่สามารถหยุดและทำงานต่อได้ในขณะที่กำลังทำงาน ใน Python กำหนดโดยใช้async defคีย์เวิร์ด เหมือนเครื่องปั่นไฟพวกเขาก็ใช้แบบฟอร์มของตัวเองซึ่งเป็นyield from awaitก่อนหน้านี้asyncและawaitถูกนำมาใช้ใน Python 3.5 เราได้สร้างโครูทีนด้วยวิธีเดียวกับที่สร้างเครื่องกำเนิดไฟฟ้า (ด้วยyield fromแทนawait)

async def inner():
    return 1

async def outer():
    await inner()

เช่นเดียวกับตัววนซ้ำหรือเครื่องกำเนิดไฟฟ้าทุกตัวที่ใช้__iter__()วิธีนี้ coroutines จะใช้งาน__await__()ซึ่งช่วยให้สามารถดำเนินการต่อได้ทุกครั้งที่await coroเรียก

มีแผนภาพลำดับที่ดีในเอกสาร Pythonที่คุณควรตรวจสอบ

ใน asyncio นอกเหนือจากฟังก์ชั่น coroutine เรามี 2 วัตถุที่สำคัญ: งานและฟิวเจอร์ส

ฟิวเจอร์ส

ฟิวเจอร์สเป็นออบเจ็กต์ที่มี__await__()การใช้วิธีการและหน้าที่ของพวกเขาคือการรักษาสถานะและผลลัพธ์ที่แน่นอน รัฐสามารถเป็นอย่างใดอย่างหนึ่งต่อไปนี้:

  1. กำลังรอ - อนาคตไม่มีผลลัพธ์หรือข้อยกเว้นใด ๆ
  2. ยกเลิก - อนาคตถูกยกเลิกโดยใช้ fut.cancel()
  3. FINISHED - อนาคตเสร็จสิ้นไม่ว่าจะโดยชุดผลลัพธ์โดยใช้fut.set_result()หรือโดยชุดข้อยกเว้นโดยใช้fut.set_exception()

ผลลัพธ์ก็เช่นเดียวกับที่คุณคาดเดาอาจเป็นวัตถุ Python ที่จะถูกส่งกลับหรือข้อยกเว้นที่อาจเพิ่มขึ้น

คุณสมบัติที่สำคัญอีกประการหนึ่งของfutureวัตถุคือมีวิธีการที่เรียกว่าadd_done_callback()วัตถุคือการที่พวกเขามีวิธีการที่เรียกว่าวิธีนี้ช่วยให้สามารถเรียกใช้ฟังก์ชันได้ทันทีที่งานเสร็จสิ้นไม่ว่าจะยกข้อยกเว้นหรือเสร็จ

งาน

วัตถุงานคือฟิวเจอร์สพิเศษซึ่งล้อมรอบโครูทีนและสื่อสารกับโครูทีนด้านในสุดและด้านนอกสุด ทุกครั้งที่มีawaitอนาคตอนาคตจะถูกส่งกลับไปที่งาน (เช่นเดียวกับในyield from) และงานจะได้รับ

ต่อไปงานจะผูกมัดตัวเองกับอนาคต โดยเรียกร้องadd_done_callback()อนาคต จากนี้ไปหากอนาคตจะเกิดขึ้นไม่ว่าจะถูกยกเลิกส่งผ่านข้อยกเว้นหรือส่งผ่านวัตถุ Python ด้วยเหตุนี้การเรียกกลับของงานจะถูกเรียกและจะกลับมามีชีวิตอีกครั้ง

Asyncio

คำถามสุดท้ายที่เราต้องตอบคือ - IO ถูกนำไปใช้อย่างไร?

asyncio ลึกลงไปเรามีห่วงเหตุการณ์ วนซ้ำเหตุการณ์ของงาน งานของ Event Loop คือการเรียกงานทุกครั้งที่พร้อมและประสานความพยายามทั้งหมดนั้นให้เป็นเครื่องทำงานเครื่องเดียว

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

เมื่อคุณพยายามรับหรือส่งข้อมูลผ่านซ็อกเก็ตผ่าน asyncio สิ่งที่เกิดขึ้นจริงด้านล่างนี้คือซ็อกเก็ตจะได้รับการตรวจสอบก่อนว่ามีข้อมูลใดที่สามารถอ่านหรือส่งได้ทันที หาก.send()บัฟเฟอร์เต็มหรือ.recv()บัฟเฟอร์ว่างซ็อกเก็ตจะถูกลงทะเบียนกับselectฟังก์ชัน (โดยการเพิ่มลงในรายการใดรายการหนึ่งrlistสำหรับrecvและwlistสำหรับsend) และฟังก์ชันที่เหมาะสมawaitที่สร้างขึ้นใหม่futureอ็อบเจ็กต์ที่ซึ่งเชื่อมโยงกับซ็อกเก็ตนั้น

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

ตอนนี้ความมหัศจรรย์ทั้งหมดเกิดขึ้น อนาคตถูกกำหนดให้เสร็จสิ้นภารกิจที่เพิ่มตัวเองมาก่อนพร้อมกับการadd_done_callback()กลับมามีชีวิตอีกครั้งและเรียกร้อง.send()ให้โครูทีนซึ่งดำเนินการต่อโครูทีนด้านในสุด (เนื่องจากawaitโซ่) และคุณอ่านข้อมูลที่ได้รับใหม่จากบัฟเฟอร์ใกล้เคียง ถูกหกไปที่

ห่วงโซ่วิธีอีกครั้งในกรณีrecv():

  1. select.select รอ.
  2. ซ็อกเก็ตที่พร้อมใช้งานพร้อมข้อมูลจะถูกส่งกลับ
  3. ข้อมูลจากซ็อกเก็ตจะถูกย้ายไปไว้ในบัฟเฟอร์
  4. future.set_result() ถูกเรียก.
  5. add_done_callback()ตอนนี้งานที่เพิ่มตัวเองด้วยถูกปลุกขึ้นมา
  6. ภารกิจเรียกโครู.send()ทีนซึ่งเข้าไปในโครูทีนด้านในสุดและปลุกมันขึ้นมา
  7. กำลังอ่านข้อมูลจากบัฟเฟอร์และส่งกลับไปยังผู้ใช้ที่ต่ำต้อยของเรา

โดยสรุปแล้ว asyncio ใช้ความสามารถของเครื่องกำเนิดไฟฟ้าซึ่งอนุญาตให้หยุดชั่วคราวและเริ่มทำงานต่อได้ ใช้yield fromความสามารถที่อนุญาตให้ส่งข้อมูลกลับไปกลับมาจากตัวสร้างด้านในสุดไปยังด้านนอกสุด ใช้สิ่งเหล่านี้ทั้งหมดเพื่อหยุดการทำงานของฟังก์ชันในขณะที่รอให้ IO เสร็จสมบูรณ์ (โดยใช้ OSselectฟังก์ชัน )

และสิ่งที่ดีที่สุด? ในขณะที่ฟังก์ชั่นหนึ่งหยุดชั่วคราวอีกฟังก์ชันหนึ่งอาจทำงานและแทรกด้วยผ้าที่บอบบางซึ่งเป็นแบบอะซิงซิโอ


13
หากต้องการคำอธิบายเพิ่มเติมอย่าลังเลที่จะแสดงความคิดเห็น Btw ฉันไม่แน่ใจว่าควรเขียนสิ่งนี้เป็นบทความบล็อกหรือคำตอบใน stackoverflow คำถามยาวเป็นคำตอบ
Bharel

1
บนซ็อกเก็ตอะซิงโครนัสการพยายามส่งหรือรับข้อมูลจะตรวจสอบบัฟเฟอร์ OS ก่อน หากคุณกำลังพยายามรับและไม่มีข้อมูลในบัฟเฟอร์ฟังก์ชันรับพื้นฐานจะส่งคืนค่าความผิดพลาดซึ่งจะเผยแพร่เป็นข้อยกเว้นใน Python เช่นเดียวกับการส่งและบัฟเฟอร์เต็ม เมื่อมีการเพิ่มข้อยกเว้น Python จะส่งซ็อกเก็ตเหล่านั้นไปยังฟังก์ชัน select ซึ่งจะระงับกระบวนการ แต่ไม่ใช่วิธีการทำงานของ asyncio แต่เป็นวิธีการเลือกและซ็อกเก็ตซึ่งเป็นระบบปฏิบัติการที่เฉพาะเจาะจงเช่นกัน
Bharel

2
@ user8371915 พร้อมช่วยเหลือเสมอ :-) โปรดทราบว่าเพื่อให้เข้าใจ Asyncio คุณต้องรู้ว่าเครื่องกำเนิดไฟฟ้าการสื่อสารของเครื่องกำเนิดไฟฟ้าและการyield fromทำงานอย่างไร อย่างไรก็ตามฉันได้ทราบไว้แล้วว่าสามารถข้ามได้ในกรณีที่ผู้อ่านรู้แล้ว :-) มีอะไรอีกที่คุณเชื่อว่าควรเพิ่ม
Bharel

2
สิ่งที่อยู่ก่อน ส่วนAsyncioอาจเป็นสิ่งที่สำคัญที่สุดเนื่องจากเป็นสิ่งเดียวที่ภาษาทำด้วยตัวเอง selectอาจมีสิทธิ์ได้เป็นอย่างดีเพราะมันเป็นวิธีการที่ไม่ปิดกั้น I / O ระบบเรียกการทำงานบน OS โครงสร้างจริงasyncioและลูปเหตุการณ์เป็นเพียงโค้ดระดับแอพที่สร้างขึ้นจากสิ่งเหล่านี้
MisterMiyagi

4
โพสต์นี้มีข้อมูลกระดูกสันหลังของ I / O แบบอะซิงโครนัสใน Python ขอบคุณสำหรับคำอธิบายที่ดี
mjkim

88

พูดถึงasync/awaitและasyncioไม่ใช่เรื่องเดียวกัน อย่างแรกคือโครงสร้างพื้นฐานระดับต่ำ (โครูทีน) ในขณะที่ต่อมาคือไลบรารีที่ใช้โครงสร้างเหล่านี้ ในทางกลับกันไม่มีคำตอบสุดท้ายเดียว

ต่อไปนี้เป็นคำอธิบายทั่วไปเกี่ยวกับการทำงานของไลบรารีasync/awaitและasyncioไลบรารี นั่นคืออาจมีเทคนิคอื่น ๆ อยู่ด้านบน (มี ... ) แต่มันไม่สำคัญเว้นแต่คุณจะสร้างขึ้นเอง ความแตกต่างควรมีเล็กน้อยเว้นแต่คุณจะรู้ดีพอที่จะไม่ต้องถามคำถามดังกล่าว

1. โครูทีนเทียบกับรูทีนย่อยในเปลือกถั่ว

เช่นเดียวกับซับรูทีน (ฟังก์ชั่นขั้นตอน ... ), coroutines (เครื่องปั่นไฟ, ... ) เป็นนามธรรมของสแต็คโทรและตัวชี้สอน: มีสแต็คของการดำเนินการชิ้นรหัสและแต่ละที่การเรียนการสอนที่เฉพาะเจาะจง

ความแตกต่างของdefเทียบกับasync defเป็นเพียงเพื่อความชัดเจน ความแตกต่างที่เกิดขึ้นจริงเมื่อเทียบกับreturn yieldจากนี้awaitหรือyield fromใช้ความแตกต่างจากการโทรแต่ละครั้งไปจนถึงสแต็กทั้งหมด

1.1. รูทีนย่อย

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

def subfoo(bar):
     qux = 3
     return qux * bar

เมื่อคุณเรียกใช้นั่นหมายความว่า

  1. จัดสรรพื้นที่สแต็กสำหรับbarและqux
  2. เรียกใช้คำสั่งแรกซ้ำแล้วข้ามไปยังคำสั่งถัดไป
  3. ครั้งหนึ่งที่returnผลักดันค่าของสแต็คโทร
  4. ล้างสแต็ก (1. ) และตัวชี้คำสั่ง (2. )

โดยเฉพาะอย่างยิ่ง 4. หมายความว่ารูทีนย่อยเริ่มต้นที่สถานะเดียวกันเสมอ ทุกอย่างที่เป็นเอกสิทธิ์ของฟังก์ชันจะหายไปเมื่อเสร็จสิ้น ไม่สามารถเรียกใช้ฟังก์ชันต่อได้แม้ว่าจะมีคำแนะนำตามมาreturnก็ตาม

root -\
  :    \- subfoo --\
  :/--<---return --/
  |
  V

1.2. โครูทีนเป็นรูทีนย่อยถาวร

โครูทีนเป็นเหมือนรูทีนย่อย แต่สามารถออกได้โดยไม่ทำลายสถานะ พิจารณาโครูทีนดังนี้:

 def cofoo(bar):
      qux = yield bar  # yield marks a break point
      return qux

เมื่อคุณเรียกใช้นั่นหมายความว่า

  1. จัดสรรพื้นที่สแต็กสำหรับbarและqux
  2. เรียกใช้คำสั่งแรกซ้ำแล้วข้ามไปยังคำสั่งถัดไป
    1. ครั้งหนึ่งที่yieldผลักดันค่าของสแต็คโทรแต่เก็บกองและตัวชี้สอน
    2. เมื่อโทรเข้าyieldให้เรียกคืนสแต็กและตัวชี้คำสั่งและส่งอาร์กิวเมนต์ไปที่qux
  3. ครั้งหนึ่งที่returnผลักดันค่าของสแต็คโทร
  4. ล้างสแต็ก (1. ) และตัวชี้คำสั่ง (2. )

หมายเหตุการเพิ่มเติมของ 2.1 และ 2.2 - โครูทีนสามารถระงับและกลับมาดำเนินการต่อได้ที่จุดที่กำหนดไว้ล่วงหน้า สิ่งนี้คล้ายกับการที่รูทีนย่อยถูกระงับระหว่างการเรียกรูทีนย่อยอื่น ความแตกต่างคือโครูทีนที่ใช้งานอยู่ไม่ได้ถูกผูกไว้อย่างเคร่งครัดกับสแต็กการโทร แต่โครูทีนที่ถูกระงับจะเป็นส่วนหนึ่งของสแต็กแยกต่างหาก

root -\
  :    \- cofoo --\
  :/--<+--yield --/
  |    :
  V    :

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

1.3. ข้ามโทรสแต็ก

จนถึงตอนนี้โครูทีนของเราจะลดระดับการโทรด้วยyield. รูทีนย่อยสามารถลงและขึ้น call stack ด้วยreturnและ(). เพื่อความสมบูรณ์โครูทีนจำเป็นต้องมีกลไกในการขึ้น call stack พิจารณาโครูทีนดังนี้:

def wrap():
    yield 'before'
    yield from cofoo()
    yield 'after'

เมื่อคุณเรียกใช้นั่นหมายความว่ามันยังคงจัดสรรสแตกและตัวชี้คำสั่งเหมือนรูทีนย่อย เมื่อระงับการทำงานนั้นจะเหมือนกับการจัดเก็บรูทีนย่อย

แต่yield fromไม่ทั้งสอง มัน suspends สแต็คและตัวชี้สอนของwrap และcofooวิ่ง โปรดทราบว่าwrapจะหยุดชั่วคราวจนกว่าจะcofooเสร็จสิ้นอย่างสมบูรณ์ เมื่อใดก็ตามที่cofooระงับหรือมีการส่งบางสิ่งจะcofooเชื่อมต่อโดยตรงกับสแต็กการโทร

1.4. โครูทีนจนสุด

ตามที่กำหนดไว้yield fromอนุญาตให้เชื่อมต่อสองขอบเขตกับอีกหนึ่งขอบเขตระดับกลาง เมื่อนำไปใช้ซ้ำนั่นหมายความว่าด้านบนของสแต็กสามารถเชื่อมต่อกับด้านล่างของสแต็กได้

root -\
  :    \-> coro_a -yield-from-> coro_b --\
  :/ <-+------------------------yield ---/
  |    :
  :\ --+-- coro_a.send----------yield ---\
  :                             coro_b <-/

สังเกตว่าrootและcoro_bไม่รู้เกี่ยวกับกันและกัน สิ่งนี้ทำให้โครูทีนสะอาดกว่าการเรียกกลับมาก: โครูทีนยังคงสร้างขึ้นบนความสัมพันธ์ 1: 1 เหมือนรูทีนย่อย Coroutines ระงับและดำเนินการต่อการดำเนินการที่มีอยู่ทั้งหมดจนกว่าจะถึงจุดเรียกใช้งานปกติ

โดยเฉพาะอย่างยิ่งrootอาจมีจำนวนโครูทีนเพื่อดำเนินการต่อ แต่ก็ไม่สามารถกลับมาทำงานต่อได้มากกว่าหนึ่งรายการในเวลาเดียวกัน โครูทีนของรูทเดียวกันพร้อมกัน แต่ไม่ขนานกัน!

1.5. Python asyncและawait

จนถึงตอนนี้คำอธิบายได้ใช้คำศัพท์yieldและyield fromคำศัพท์ของเครื่องกำเนิดไฟฟ้าอย่างชัดเจน- ฟังก์ชันพื้นฐานเหมือนกัน ไวยากรณ์ Python3.5 ใหม่asyncและawaitมีไว้เพื่อความชัดเจนเป็นหลัก

def foo():  # subroutine?
     return None

def foo():  # coroutine?
     yield from foofoo()  # generator? coroutine?

async def foo():  # coroutine!
     await foofoo()  # coroutine!
     return None

async forและasync withงบที่มีความจำเป็นเพราะคุณจะทำลายyield from/awaitห่วงโซ่กับเปลือยforและwithงบ

2. กายวิภาคของห่วงเหตุการณ์อย่างง่าย

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

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

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

loop -\
  :    \-> coroutine --await--> event --\
  :/ <-+----------------------- yield --/
  |    :
  |    :  # loop waits for event to happen
  |    :
  :\ --+-- send(reply) -------- yield --\
  :        coroutine <--yield-- event <-/

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

2.1.1. เหตุการณ์ในช่วงเวลา

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

2.1.2. การกำหนดเหตุการณ์

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

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)

คลาสนี้จัดเก็บเฉพาะเหตุการณ์เท่านั้นไม่ได้บอกว่าจะจัดการอย่างไร

คุณสมบัติพิเศษเพียงอย่างเดียวคือ__await__- เป็นสิ่งที่awaitคีย์เวิร์ดมองหา ในทางปฏิบัติมันเป็นเครื่องวนซ้ำ แต่ไม่สามารถใช้ได้กับเครื่องจักรการทำซ้ำแบบปกติ

2.2.1. กำลังรอกิจกรรม

ตอนนี้เรามีเหตุการณ์แล้วโครูทีนมีปฏิกิริยาอย่างไร? เราควรจะสามารถแสดงสิ่งที่เทียบเท่าได้sleepโดยการเข้าawaitร่วมกิจกรรมของเรา เพื่อดูว่าเกิดอะไรขึ้นเรารอสองครั้งครึ่งหนึ่ง:

import time

async def asleep(duration: float):
    """await that ``duration`` seconds pass"""
    await AsyncSleep(time.time() + duration / 2)
    await AsyncSleep(time.time() + duration / 2)

เราสามารถสร้างอินสแตนซ์และเรียกใช้โครูทีนนี้ได้โดยตรง คล้ายกับเครื่องกำเนิดไฟฟ้าโดยใช้โครูcoroutine.sendทีนจนกว่าจะได้yieldผลลัพธ์

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

สิ่งนี้ทำให้เรามีสองAsyncSleepเหตุการณ์และStopIterationเมื่อโครูทีนเสร็จสิ้น สังเกตว่าความล่าช้าเพียงอย่างเดียวมาจากtime.sleepในลูป! แต่ละรายการAsyncSleepจะจัดเก็บเฉพาะค่าชดเชยจากเวลาปัจจุบัน

2.2.2. เหตุการณ์ + นอน

ณ จุดนี้เรามีกลไกสองอย่างที่แยกจากกัน:

  • AsyncSleep เหตุการณ์ที่เกิดขึ้นได้จากภายในโครูทีน
  • time.sleep ที่รอได้โดยไม่ส่งผลกระทบต่อโครูทีน

โดยเฉพาะอย่างยิ่งสองสิ่งนี้เป็นมุมฉาก: ไม่มีใครส่งผลกระทบหรือกระตุ้นอีกฝ่าย เป็นผลให้เราสามารถกำหนดกลยุทธ์ของเราเองsleepเพื่อตอบสนองความล่าช้าของAsyncSleepไฟล์.

2.3. ห่วงเหตุการณ์ไร้เดียงสา

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

สิ่งนี้ทำให้การตั้งเวลาตรงไปตรงมา:

  1. เรียงลำดับตามเวลาตื่นนอนที่ต้องการ
  2. เลือกคนแรกที่อยากตื่น
  3. รอจนกว่าจะถึงเวลานี้
  4. เรียกใช้โครูทีนนี้
  5. ทำซ้ำจาก 1.

การใช้งานเล็กน้อยไม่จำเป็นต้องมีแนวคิดขั้นสูงใด ๆ A listอนุญาตให้จัดเรียงโครูทีนตามวันที่ time.sleepรอเป็นปกติ การรันโครูทีนใช้งานได้เหมือนก่อนหน้า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])

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

2.4. รอสหกรณ์

AsyncSleepเหตุการณ์และrunห่วงเหตุการณ์ที่มีการดำเนินงานที่ทำงานอย่างเต็มที่ของเหตุการณ์หมดเวลา

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

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

3. I / O ลูปเหตุการณ์

ห่วงเหตุการณ์ที่สนับสนุนsleepเหมาะสำหรับการลงคะแนนเลือกตั้ง อย่างไรก็ตามการรอ I / O บนที่จับไฟล์สามารถทำได้อย่างมีประสิทธิภาพมากขึ้น: ระบบปฏิบัติการใช้ I / O และทำให้รู้ว่าแฮนเดิลใดพร้อมใช้งาน ตามหลักการแล้วการวนซ้ำของเหตุการณ์ควรสนับสนุนเหตุการณ์ "พร้อมสำหรับ I / O" ที่ชัดเจน

3.1. selectโทร

Python มีอินเทอร์เฟซสำหรับสืบค้น OS สำหรับการจัดการการอ่าน I / O อยู่แล้ว เมื่อเรียกด้วยแฮนเดิลเพื่ออ่านหรือเขียนมันจะส่งกลับที่จับพร้อมที่จะอ่านหรือเขียน:

readable, writeable, _ = select.select(rlist, wlist, xlist, timeout)

ตัวอย่างเช่นเราสามารถopenเขียนไฟล์และรอให้พร้อม:

write_target = open('/tmp/foo')
readable, writeable, _ = select.select([], [write_target], [])

เมื่อเลือกผลตอบแทนแล้วจะwriteableมีไฟล์ที่เปิดอยู่

3.2. เหตุการณ์ I / O พื้นฐาน

คล้ายกับAsyncSleepคำขอเราจำเป็นต้องกำหนดเหตุการณ์สำหรับ I / O ด้วยselectตรรกะพื้นฐานเหตุการณ์ต้องอ้างถึงวัตถุที่อ่านได้ - พูดopenไฟล์ นอกจากนี้เราจัดเก็บข้อมูลที่จะอ่าน

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

เช่นเดียวกับที่AsyncSleepเราจัดเก็บข้อมูลที่จำเป็นสำหรับการเรียกใช้ระบบพื้นฐานเป็นส่วนใหญ่ คราว__await__นี้สามารถกลับมาอ่านซ้ำได้หลายครั้งจนกว่าจะamountอ่านสิ่งที่ต้องการ นอกจากนี้เราreturnยังให้ผลลัพธ์ I / O แทนที่จะดำเนินการต่อ

3.3. การเพิ่มลูปเหตุการณ์ด้วยการอ่าน I / O

พื้นฐานสำหรับการวนซ้ำเหตุการณ์ของเรายังคงเป็นที่runกำหนดไว้ก่อนหน้านี้ อันดับแรกเราต้องติดตามคำขออ่าน นี่ไม่ใช่กำหนดการที่จัดเรียงอีกต่อไปเราทำแผนที่เฉพาะคำขออ่านเพื่อจัดเรียง

# new
waiting_read = {}  # type: Dict[file, coroutine]

เนื่องจากselect.selectใช้พารามิเตอร์การหมดเวลาเราจึงสามารถใช้แทนtime.sleepได้

# old
time.sleep(max(0.0, until - time.time()))
# new
readable, _, _ = select.select(list(reads), [], [])

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

# new - reschedule waiting coroutine, run readable coroutine
if readable:
    waiting.append((until, coroutine))
    waiting.sort()
    coroutine = waiting_read[readable[0]]

สุดท้ายเราต้องฟังคำขออ่านจริงๆ

# new
if isinstance(command, AsyncSleep):
    ...
elif isinstance(command, AsyncRead):
    ...

3.4. วางไว้ด้วยกัน

ข้างต้นเป็นการทำให้เข้าใจง่ายขึ้นเล็กน้อย เราจำเป็นต้องสลับไปมาเพื่อไม่ให้อดอาหารถ้าเราสามารถอ่านได้ตลอดเวลา เราจำเป็นต้องจัดการโดยไม่มีอะไรจะอ่านหรือไม่มีอะไรให้รอ อย่างไรก็ตามผลลัพธ์สุดท้ายยังคงอยู่ใน 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 สหกรณ์

ขณะAsyncSleepนี้AsyncReadและrunการใช้งานสามารถใช้งานได้อย่างสมบูรณ์เพื่อเข้าสู่โหมดสลีปและ / หรืออ่าน เช่นเดียวกับsleepyเราสามารถกำหนดผู้ช่วยเพื่อทดสอบการอ่าน:

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

เมื่อเรียกใช้สิ่งนี้เราจะเห็นว่า I / O ของเราถูกแทรกแซงด้วยงานที่รออยู่:

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. Non-Blocking I / O

ในขณะที่ I / O ในแฟ้มได้รับแนวคิดข้ามมันไม่ได้จริงๆเหมาะสำหรับห้องสมุดเหมือนasyncioที่: selectโทรผลตอบแทนเสมอสำหรับไฟล์และทั้งสองopenและreadอาจป้องกันการไปเรื่อย ๆ สิ่งนี้จะบล็อกโครูทีนทั้งหมดของลูปเหตุการณ์ซึ่งไม่ดี ไลบรารีเช่นaiofilesใช้เธรดและการซิงโครไนซ์กับ I / O ที่ไม่ปิดกั้นปลอมและเหตุการณ์ในไฟล์

อย่างไรก็ตามซ็อกเก็ตอนุญาตให้มี I / O แบบไม่ปิดกั้น - และเวลาแฝงที่มีมาโดยกำเนิดทำให้มีความสำคัญมากขึ้น เมื่อใช้ในการวนซ้ำของเหตุการณ์คุณสามารถรวมการรอข้อมูลและการลองใหม่ได้โดยไม่ปิดกั้นสิ่งใด

4.1. เหตุการณ์ I / O ที่ไม่ปิดกั้น

เช่นเดียวกับของAsyncReadเราเราสามารถกำหนดเหตุการณ์ระงับและอ่านสำหรับซ็อกเก็ต แทนที่จะใช้ไฟล์เราใช้ซ็อกเก็ตซึ่งต้องไม่ปิดกั้น นอกจากนี้การ__await__ใช้งานของเราsocket.recvแทน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)
        )

ในทางตรงกันข้ามกับAsyncRead, __await__ดำเนินการอย่างแท้จริง non-blocking I / O เมื่อมีข้อมูลก็จะอ่านเสมอ เมื่อไม่มีข้อมูลก็จะระงับเสมอ นั่นหมายถึงการวนซ้ำเหตุการณ์จะถูกบล็อกในขณะที่เราทำงานที่มีประโยชน์

4.2. ยกเลิกการปิดกั้นลูปเหตุการณ์

เท่าที่เป็นห่วงเหตุการณ์ไม่มีอะไรเปลี่ยนแปลงมากนัก เหตุการณ์ที่จะรับฟังยังคงเหมือนกับไฟล์ - ตัวอธิบายไฟล์ที่ทำเครื่องหมายว่าพร้อม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

ณ จุดนี้น่าจะชัดเจนAsyncReadและAsyncRecvเป็นเหตุการณ์ประเภทเดียวกัน เราสามารถ refactor ให้เป็นเหตุการณ์เดียวด้วยส่วนประกอบ I / O ที่แลกเปลี่ยนได้ ผลที่ตามมาการวนซ้ำเหตุการณ์โครูทีนและเหตุการณ์จะแยกตัวกำหนดตารางเวลารหัสกลางโดยพลการและ I / O จริงอย่างชัดเจน

4.3. ด้านที่น่าเกลียดของ I / O ที่ไม่ปิดกั้น

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

# 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

เรื่องสั้นขนาดยาวสิ่งที่เหลืออยู่คือการจัดการข้อยกเว้นไม่กี่สิบบรรทัด เหตุการณ์และการวนซ้ำของเหตุการณ์ทำงาน ณ จุดนี้แล้ว

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

ภาคผนวก

ตัวอย่างรหัสที่ github


การใช้yield selfใน AsyncSleep ทำให้ฉันมีTask got back yieldข้อผิดพลาดทำไมจึงเป็นเช่นนั้น ฉันเห็นว่ารหัสใน asyncio Futures ใช้สิ่งนั้น การใช้ผลผลิตเปล่าได้ผลดี
Ron Serruya

1
ลูปเหตุการณ์มักจะคาดหวังเฉพาะเหตุการณ์ของตนเอง โดยทั่วไปคุณไม่สามารถผสมเหตุการณ์และลูปเหตุการณ์ในไลบรารีได้ เหตุการณ์ที่แสดงที่นี่ใช้ได้เฉพาะกับการวนซ้ำเหตุการณ์ที่แสดง โดยเฉพาะ asyncio ใช้เฉพาะ None (เช่นผลตอบแทนเปล่า) เป็นสัญญาณสำหรับลูปเหตุการณ์ เหตุการณ์โต้ตอบโดยตรงกับวัตถุห่วงเหตุการณ์เพื่อลงทะเบียนการปลุก
MisterMiyagi

12

การcoroตัดสินใจของคุณถูกต้องตามแนวคิด แต่ไม่สมบูรณ์เล็กน้อย

awaitไม่ระงับโดยไม่มีเงื่อนไข แต่เฉพาะในกรณีที่พบการโทรที่ปิดกั้น จะรู้ได้อย่างไรว่ามีการบล็อกการโทร สิ่งนี้ถูกตัดสินโดยรหัสที่รออยู่ ตัวอย่างเช่นการใช้งานซ็อกเก็ตการอ่านที่รอคอยอาจถูกกำหนดไว้ที่:

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

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

ในด้านผู้โทรเมื่อโครูทีนของคุณประกอบด้วย:

data = await read(sock, 1024)

มัน desugars เป็นสิ่งที่ใกล้เคียงกับ:

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

คนที่คุ้นเคยกับเครื่องกำเนิดไฟฟ้ามักจะอธิบายข้างต้นในแง่ของyield fromการระงับโดยอัตโนมัติ

โซ่กันสะเทือนจะดำเนินต่อไปจนถึงลูปเหตุการณ์ซึ่งสังเกตเห็นว่าโครูทีนถูกระงับเอาออกจากชุดที่รันได้และดำเนินการโครูทีนที่รันได้ถ้ามี หากไม่สามารถรันโคโรทีนได้ลูปจะรอselect()จนกว่าตัวอธิบายไฟล์ที่โครูทีนสนใจจะพร้อมสำหรับ IO (ลูปเหตุการณ์รักษาการแมป file-descriptor-to-coroutine)

ในตัวอย่างข้างต้นเมื่อselect()บอกลูปเหตุการณ์ที่sockสามารถอ่านได้แล้วมันจะเพิ่มcoroเข้าไปในชุดที่รันได้อีกครั้งดังนั้นจึงจะดำเนินการต่อจากจุดที่ถูกระงับ

กล่าวอีกนัยหนึ่ง:

  1. ทุกอย่างเกิดขึ้นในชุดข้อความเดียวกันตามค่าเริ่มต้น

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

สำหรับข้อมูลเชิงลึกเกี่ยวกับการวนรอบเหตุการณ์ที่ขับเคลื่อนด้วยโครูทีนฉันขอแนะนำการพูดคุยนี้โดย Dave Beazley ซึ่งเขาสาธิตการเขียนโค้ดวนเหตุการณ์ตั้งแต่เริ่มต้นต่อหน้าผู้ชมสด


ขอบคุณนี่เป็นสิ่งที่ใกล้เคียงกับสิ่งที่ฉันตามมามากขึ้น แต่ยังไม่สามารถอธิบายได้ว่าทำไมasync.wait_for()ไม่ทำในสิ่งที่ควรทำ ... เหตุใดจึงเป็นปัญหาใหญ่ในการเพิ่มการเรียกกลับไปยังเหตุการณ์และบอกมัน ในการประมวลผลการเรียกกลับจำนวนมากที่ต้องการรวมถึงการโทรกลับที่คุณเพิ่งเพิ่ม? ความไม่พอใจของฉันasyncioส่วนหนึ่งเป็นผลมาจากความจริงที่ว่าแนวคิดพื้นฐานนั้นง่ายมากและตัวอย่างเช่น Emacs Lisp มีการใช้งานเป็นเวลานานโดยไม่ต้องใช้คำศัพท์ ... (เช่นcreate-async-processและaccept-process-output- และนี่คือทั้งหมดที่จำเป็น ... (ต่อ)
wvxvw

10
@wvxvw ฉันได้ตอบคำถามที่คุณโพสต์มากที่สุดเท่าที่จะทำได้เนื่องจากมีเพียงย่อหน้าสุดท้ายเท่านั้นที่มีคำถามหกข้อ ดังนั้นเราจึงดำเนินต่อไป - ไม่ใช่ว่าwait_for จะไม่ทำในสิ่งที่ควรจะเป็น (มันเป็นสิ่งที่คุณควรรอคอย) ความคาดหวังของคุณไม่ตรงกับสิ่งที่ระบบได้รับการออกแบบและนำไปใช้ ฉันคิดว่าปัญหาของคุณสามารถจับคู่กับ asyncio ได้หากการวนซ้ำของเหตุการณ์ทำงานในเธรดแยกต่างหาก แต่ฉันไม่รู้รายละเอียดของกรณีการใช้งานของคุณและโดยสุจริตทัศนคติของคุณไม่ได้ทำให้การช่วยเหลือคุณเป็นเรื่องสนุก
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...- ไม่มีอะไรหยุดคุณจากการนำแนวคิดง่ายๆนี้ไปใช้โดยไม่มี buzzwords สำหรับ Python แล้ว :) ทำไมคุณถึงใช้ asyncio ที่น่าเกลียดนี้เลย? ใช้งานของคุณเองตั้งแต่เริ่มต้น ตัวอย่างเช่นคุณสามารถเริ่มต้นด้วยการสร้างasync.wait_for()ฟังก์ชันของคุณเองที่ทำในสิ่งที่ควรจะเป็น
Mikhail Gerasimov

1
@MikhailGerasimov ดูเหมือนคุณจะคิดว่ามันเป็นคำถามเกี่ยวกับวาทศิลป์ แต่ฉันอยากจะปัดเป่าความลึกลับให้คุณ ภาษาถูกออกแบบมาเพื่อพูดกับผู้อื่น ฉันไม่สามารถเลือกให้คนอื่นพูดภาษาได้แม้ว่าฉันจะเชื่อว่าภาษาที่พวกเขาพูดนั้นเป็นขยะ แต่สิ่งที่ดีที่สุดที่ฉันทำได้คือพยายามโน้มน้าวพวกเขาในกรณีนี้ ในคำอื่น ๆ asyncioถ้าฉันเป็นอิสระที่จะเลือกผมไม่เคยเลือกงูใหญ่จะเริ่มต้นด้วยให้อยู่คนเดียว แต่โดยหลักการแล้วนั่นไม่ใช่การตัดสินใจของฉัน ผมบังคับให้ใช้ภาษาขยะผ่านen.wikipedia.org/wiki/Ultimatum_game
wvxvw

4

ทุกอย่างขัดแย้งกับความท้าทายหลักสองประการที่ asyncio กล่าวถึง:

  • วิธีดำเนินการหลาย I / O ในเธรดเดียว
  • จะใช้งานมัลติทาสก์แบบร่วมมือได้อย่างไร?

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

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

แหล่งข้อมูลเพิ่มเติมในคำตอบนี้


แก้ไข:การแสดงความคิดเห็นของคุณเกี่ยวกับ goroutines:

สิ่งที่ใกล้เคียงที่สุดกับ goroutine ใน asyncio ไม่ใช่ coroutine แต่เป็นงาน (ดูความแตกต่างในเอกสารประกอบ ) ใน python โครูทีน (หรือเครื่องกำเนิดไฟฟ้า) ไม่รู้อะไรเกี่ยวกับแนวคิดของลูปเหตุการณ์หรือ I / O เป็นเพียงฟังก์ชั่นที่สามารถหยุดการทำงานโดยใช้yieldในขณะที่ยังคงสถานะปัจจุบันไว้ดังนั้นจึงสามารถเรียกคืนได้ในภายหลัง yield fromไวยากรณ์ช่วยให้การผูกมัดพวกเขาในทางที่โปร่งใส

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


แก้ไข:ตอบคำถามบางส่วนในโพสต์ของคุณ:

I / O เกิดขึ้นจริงในสถานการณ์นี้อย่างไร ในกระทู้แยกกัน? ล่ามทั้งหมดถูกระงับและ I / O เกิดขึ้นนอกล่ามหรือไม่

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

I / O หมายถึงอะไรกันแน่? หากโพรซีเดอร์ python ของฉันเรียกว่า C open () โพรซีเดอร์และในทางกลับกันก็ส่งการขัดจังหวะไปยังเคอร์เนลโดยยกเลิกการควบคุมมันล่าม Python รู้ได้อย่างไรเกี่ยวกับเรื่องนี้และสามารถรันโค้ดอื่น ๆ ต่อไปได้ในขณะที่โค้ดเคอร์เนลทำ I / จริง O และจนกว่าจะปลุกขั้นตอน Python ซึ่งส่งการขัดจังหวะมา แต่เดิม? โดยหลักการแล้วล่าม Python จะระวังเหตุการณ์นี้ได้อย่างไร?

I / O คือการโทรที่ปิดกั้น ใน asyncio การดำเนินการ I / O ทั้งหมดควรดำเนินไปตามลูปเหตุการณ์เพราะอย่างที่คุณกล่าวไว้ว่าการวนซ้ำของเหตุการณ์ไม่มีทางที่จะทราบได้ว่ามีการดำเนินการเรียกบล็อกในรหัสซิงโครนัส นั่นหมายความว่าคุณไม่ควรใช้ซิงโครนัสopenภายในบริบทของโครูทีน ให้ใช้ไลบรารีเฉพาะเช่นaiofilesซึ่งมีเวอร์ชันอะซิงโครนัสของopen.


การบอกว่ามีการใช้โครูทีนโดยใช้yield fromไม่ได้พูดอะไรเลย yield fromเป็นเพียงโครงสร้างไวยากรณ์ไม่ใช่โครงสร้างพื้นฐานที่คอมพิวเตอร์สามารถเรียกใช้งานได้ ในทำนองเดียวกันสำหรับลูปเลือก ใช่โครูทีนใน Go ยังใช้ลูปแบบเลือก แต่สิ่งที่ฉันพยายามทำจะใช้ได้ผลใน Go แต่ใช้ไม่ได้ใน Python ฉันต้องการคำตอบโดยละเอียดเพื่อทำความเข้าใจว่าเหตุใดจึงไม่ได้ผล
wvxvw

ขอโทษ ... ไม่ไม่จริง "อนาคต", "งาน", "ทางโปร่งใส", "ผลตอบแทนจาก" เป็นเพียงคำศัพท์ที่ไม่ได้มาจากโดเมนของการเขียนโปรแกรม การเขียนโปรแกรมมีตัวแปรขั้นตอนและโครงสร้าง ดังนั้นการพูดว่า "goroutine is a task" เป็นเพียงคำชี้แจงแบบวงกลมที่ทำให้เกิดคำถาม ในท้ายที่สุดคำอธิบายว่าasyncioสำหรับฉันแล้วจะทำให้โค้ด C เป็นอย่างไรซึ่งแสดงให้เห็นว่าไวยากรณ์ของ Python ถูกแปลเป็นอะไร
wvxvw

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

7
@wvxvw ฉันไม่เห็นด้วย สิ่งเหล่านี้ไม่ใช่ "คำศัพท์" แต่เป็นแนวคิดระดับสูงที่ถูกนำไปใช้ในห้องสมุดหลายแห่ง ตัวอย่างเช่นงาน asyncio, gevent greenlet และ goroutine ทั้งหมดเกี่ยวข้องกับสิ่งเดียวกันนั่นคือหน่วยการดำเนินการที่สามารถทำงานพร้อมกันภายในเธรดเดียว นอกจากนี้ฉันไม่คิดว่า C จำเป็นต้องเข้าใจ asyncio เลยเว้นแต่คุณต้องการเข้าสู่การทำงานภายในของเครื่องกำเนิด python
Vincent

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