Python 3.5'te Coroutine ve Future / Görev arasındaki fark nedir?


102

Diyelim ki kukla bir fonksiyonumuz var:

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

Aradaki fark nedir:

import asyncio    

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

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

Ve:

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

Not : Örnek bir sonuç döndürür, ancak sorunun odak noktası bu değildir. Dönüş değeri önemli olduğunda gather()bunun yerine kullanın wait().

Geri dönüş değeri ne olursa olsun, açıklık arıyorum ensure_future(). wait(coros)ve wait(futures)her ikisi de eşdizimleri çalıştırır, öyleyse bir eşgüdüm ne zaman ve neden sarılmalıdır ensure_future?

Temel olarak, Python 3.5'i kullanarak bir dizi engellemesiz işlemi çalıştırmak için Doğru Yol (tm) asyncnedir?

Ekstra kredi için, aramaları gruplandırmak istersem ne olur? Örneğin some_remote_call(...)1000 kez aramam gerekiyor ama web sunucusunu / veritabanını / vb. 1000 eşzamanlı bağlantıyla ezmek istemiyorum. Bu bir iş parçacığı veya işlem havuzu ile yapılabilir, ancak bunu yapmanın bir yolu var asynciomı?

2020 güncellemesi (Python 3.7+) : Bu snippet'leri kullanmayın. Bunun yerine şunu kullanın:

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)

Ayrıca kullanmayı düşünün Trio , asyncio için sağlam bir 3. parti bir alternatif.

Yanıtlar:


97

Bir koroutin, hem değerleri veren hem de dışarıdan değerleri kabul edebilen bir oluşturucu işlevdir. Bir coroutine kullanmanın yararı, bir fonksiyonun yürütülmesini duraklatıp daha sonra devam ettirebilmemizdir. Bir ağ işlemi durumunda, biz yanıt beklerken bir işlevin yürütülmesini duraklatmak mantıklıdır. Zamanı diğer bazı işlevleri çalıştırmak için kullanabiliriz.

Gelecek, PromiseJavascript'teki nesneler gibidir . Gelecekte gerçekleşecek bir değer için yer tutucu gibidir. Yukarıda bahsedilen durumda, ağ G / Ç'sini beklerken, bir işlev bize bir kap verebilir, işlem tamamlandığında kabı değerle dolduracağına dair bir söz verebilir. Gelecekteki nesneyi tutuyoruz ve yerine getirildiğinde, asıl sonucu almak için onun üzerinde bir yöntem çağırabiliriz.

Doğrudan Cevap: Sen gerekmez ensure_futuresonuçları gerekmiyorsa. Sonuçlara ihtiyacınız varsa veya oluşan istisnaları alırsanız iyidirler.

Ekstra Kredi: Maksimum çalışan sayısını kontrol etmek için run_in_executorbir Executorörnek seçer ve geçerim .

Açıklamalar ve Örnek kodlar

İlk örnekte, eş anlamlılar kullanıyorsunuz. waitFonksiyon değiş tokuş eden kavramlar bir demet alır ve bunları birbirine birleştirir. Böylece wait()tüm eşgörünümler tükendiğinde biter (tüm değerleri döndürerek tamamlanır / tamamlanır).

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

run_until_completeYöntem yürütme tamamlanana kadar döngü hayatta olduğundan emin olmak istiyorum. Lütfen bu durumda zaman uyumsuz yürütmenin sonuçlarını nasıl almadığınıza dikkat edin.

İkinci örnekte, ensure_futurebir eşdizimi sarmak ve Taskbir tür olan bir nesneyi döndürmek için işlevi kullanıyorsunuz Future. Koroutin, aradığınızda ana olay döngüsünde yürütülmek üzere planlanır ensure_future. Döndürülen gelecek / görev nesnesinin henüz bir değeri yoktur, ancak zamanla, ağ işlemleri bittiğinde, gelecekteki nesne işlemin sonucunu tutacaktır.

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

Yani bu örnekte, aynı şeyi yapıyoruz, tek fark, sadece eş eşyalar yerine vadeli işlemleri kullanıyoruz.

Asyncio / coroutines / futures'ın nasıl kullanılacağına dair bir örneğe bakalım:

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

Burada nesne create_tasküzerindeki yöntemi kullandık loop. ensure_futuregörevi ana olay döngüsünde planlar. Bu yöntem, seçtiğimiz bir döngüde bir koroutin planlamamızı sağlar.

Ayrıca add_done_callback, görev nesnesinde yöntemi kullanarak bir geri arama ekleme kavramını da görüyoruz .

A Task, donecoroutinin bir değer döndürdüğü, bir istisna oluşturduğu veya iptal edildiği zamandır. Bu olayları kontrol etmenin yöntemleri vardır.

Bu konularda yardımcı olabilecek bazı blog yazıları yazdım:

Elbette, resmi kılavuzda daha fazla ayrıntı bulabilirsiniz: https://docs.python.org/3/library/asyncio.html


3
Sorumu biraz daha net olacak şekilde güncelledim - koroutinden sonuca ihtiyacım yoksa yine de kullanmam gerekir ensure_future()mi? Ve sonuca ihtiyacım olursa, kullanamaz run_until_complete(gather(coros))mıyım?
knite

1
ensure_futureKoroutini olay döngüsünde yürütülecek şekilde planlar. Bu yüzden evet derdim, bu gerekli. Ama tabii ki diğer işlevleri / yöntemleri kullanarak da eşgüdümleri planlayabilirsiniz. Evet, kullanabilirsiniz gather()- ancak toplama tüm yanıtlar toplanana kadar bekleyecektir.
masnun

5
@AbuAshrafMasnun @knite gatherve waitverilen eşgüdümleri kullanarak görevler olarak sarmalayın ensure_future( buradaki ve buradaki kaynaklara bakın ). Yani ensure_futureönceden kullanmanın bir anlamı yok ve sonuçları almak ya da almamakla ilgisi yok.
Vincent

8
@AbuAshrafMasnun @knite Ayrıca ensure_futurebir loopargüman var, bu yüzden loop.create_taskover kullanmak için bir neden yok ensure_future. Ve run_in_executoreşdizimlerle çalışmaz, onun yerine semafor kullanılmalıdır.
Vincent

2
@vincent create_taskover kullanmak için bir neden var ensure_future, bkz. belgeler . Alıntıcreate_task() (added in Python 3.7) is the preferable way for spawning new tasks.
masi

24

Basit cevap

  • Bir coroutine işlevini ( async def) çağırmak onu ÇALIŞTIRMAZ. Oluşturucu işlevinin jeneratör nesnelerini döndürmesi gibi, bir koroutin nesnesi döndürür.
  • await eşdizimlerden değerleri alır, yani eş dizini "çağırır"
  • eusure_future/create_task coroutini sonraki yinelemede olay döngüsünde çalışacak şekilde zamanlayın (bir daemon iş parçacığı gibi bitmelerini beklemese de).

Bazı kod örnekleri

Önce bazı terimleri açıklayalım:

  • coroutine işlevi, siz olduğunuz async def;
  • coroutine nesnesi, bir coroutine işlevi "çağırdığınızda" elde ettiğiniz şey;
  • görev, olay döngüsünde çalıştırılacak bir coroutine nesnesinin etrafına sarılmış bir nesne.

Durum 1, awaitbir koroutinde

Biri iki coroutine oluşturuyoruz ve diğerini çalıştırmak için awaitkullanıyoruz 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())

sonuç alacaksın:

1539486251.7055213 - await
1539486251.7055705 - create_task

Açıklamak:

task1 doğrudan yürütüldü ve görev2 aşağıdaki yinelemede yürütüldü.

Durum 2, kontrolün olay döngüsüne aktarılması

Ana işlevi değiştirirsek, farklı bir sonuç görebiliriz:

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

sonuç alacaksın:

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

Açıklamak:

Çağırma asyncio.sleep(1)sırasında, kontrol olay döngüsüne geri verildi ve döngü, çalışacak görevleri denetler, ardından tarafından oluşturulan görevi çalıştırır create_task.

İlk önce coroutine fonksiyonunu çağırdığımızı ama awaitonu çağırmadığımızı unutmayın , bu yüzden sadece tek bir coroutine yarattık ve onu çalıştırmıyoruz. Ardından, coroutine işlevini yeniden çağırırız ve onu bir create_taskçağrıya sararız, creat_task aslında coroutini bir sonraki yinelemede çalışacak şekilde planlayacaktır. Yani sonuçta create taskdaha önce yürütülür await.

Aslında buradaki nokta, döngüye kontrolü geri vermektir asyncio.sleep(0), aynı sonucu görmek için kullanabilirsiniz .

Kaputun altında

loop.create_taskaslında arar asyncio.tasks.Task(), hangi arayacak loop.call_soon. Ve loop.call_soongörevi yerine getirecek loop._ready. Döngünün her yinelemesi sırasında, loop._ready içindeki her geri aramayı denetler ve çalıştırır.

asyncio.wait, asyncio.ensure_futureVe asyncio.gatheraslında çağrı loop.create_taskdoğrudan veya dolaylı olarak.

Ayrıca belgelerde şunları not edin :

Geri aramalar, kayıtlı oldukları sıraya göre çağrılır. Her geri arama tam olarak bir kez çağrılacaktır.


1
Temiz bir açıklama için teşekkürler! Söylemeliyim ki, oldukça korkunç bir tasarım. Yüksek seviyeli API, API'yi aşırı karmaşık hale getiren düşük seviyeli soyutlamayı sızdırıyor.
Boris Burkov

1
iyi tasarlanmış merak projesine göz
atın

Güzel açıklama! await task2Aramanın etkisinin açıklığa kavuşturulabileceğini düşünüyorum. Her iki örnekte de, loop.create_task () çağrısı, olay döngüsünde task2'yi zamanlayan şeydir. Yani her iki await task2exs'de de silebilirsiniz ve yine de task2 sonunda çalışacaktır. Ex2'de davranış aynı olacaktır, zira await task2zaten tamamlanmış görevi zamanlamak (ikinci kez çalışmayacaktır), oysa ex1'de ana görev tamamlanana kadar görev2 yürütülmeyeceği için davranış biraz farklı olacaktır. Farkı görmek için print("end of main"), ex1 ana ekranının sonuna ekleyin
Andrew

11

Vincent'ın https://github.com/python/asyncio/blob/master/asyncio/tasks.py#L346 bağlantısına bağlı bir yorumu , sizin wait()için eşgüdümleri sarmaladığını gösteriyor ensure_future()!

Başka bir deyişle, bir geleceğe ihtiyacımız var ve yordamlar sessizce onlara dönüşecek.

Karşılıklı / vadeli işlemlerin nasıl gruplandırılacağına dair kesin bir açıklama bulduğumda bu yanıtı güncelleyeceğim.


Bir eşyordam nesne için anlamına mı c, await ceşdeğerdir await create_task(c)?
Alexey

3

BDFL'den [2013]

Görevler

  • Bu bir Geleceğe sarılmış bir koroutin
  • class Task, Future sınıfının bir alt sınıfıdır
  • Yani await ile de çalışır !

  • Çıplak bir coroutinden farkı nedir?
  • Beklemeden ilerleme kaydedebilir
    • Başka bir şey beklediğiniz sürece, yani
      • bekliyorum [başka bir şey]

Bunu akılda tutarak, ensure_futurebir Görev oluşturmak için bir isim olarak anlamlıdır, çünkü Geleceğin sonucu siz bekleseniz de beklemeseniz de hesaplanacaktır (bir şeyi beklediğiniz sürece). Bu, siz başka şeyleri beklerken olay döngüsünün Görevinizi tamamlamasını sağlar. Python 3.7'de bir gelecek sağlamakcreate_task için tercih edilen yoldur .

Not: Guido'nun slaytlarındaki "verimi" modernite için burada "bekle" olarak değiştirdim.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.