Perbedaan antara coroutine dan future / task dengan Python 3.5?

100

Katakanlah kita memiliki fungsi dummy:

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

Apa perbedaan antara:

import asyncio    

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

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

Dan:

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

Catatan : Contoh mengembalikan hasil, tetapi ini bukan fokus pertanyaan. Ketika hal-hal nilai kembali, menggunakan gather()bukan wait().

Terlepas dari nilai pengembaliannya, saya mencari kejelasan tentang ensure_future(). wait(coros)dan wait(futures)keduanya menjalankan coroutine, jadi kapan dan mengapa coroutine harus digabungkan ensure_future?

Pada dasarnya, apa Cara yang Benar (tm) untuk menjalankan banyak operasi non-pemblokiran menggunakan Python 3.5 async?

Untuk kredit ekstra, bagaimana jika saya ingin melakukan batch panggilan? Misalnya, saya perlu menelepon some_remote_call(...)1000 kali, tetapi saya tidak ingin menghancurkan server web / database / dll dengan 1000 koneksi simultan. Ini dapat dilakukan dengan utas atau kumpulan proses, tetapi adakah cara untuk melakukannya asyncio?

Pembaruan 2020 (Python 3.7+) : Jangan gunakan cuplikan ini. Sebagai gantinya gunakan:

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)

Pertimbangkan juga untuk menggunakan Trio , alternatif pihak ketiga yang tangguh untuk asyncio.

rajutan
sumber

Jawaban:

95

Coroutine adalah fungsi generator yang dapat menghasilkan nilai dan menerima nilai dari luar. Manfaat menggunakan coroutine adalah kita dapat menjeda eksekusi suatu fungsi dan melanjutkannya nanti. Dalam kasus operasi jaringan, masuk akal untuk menjeda eksekusi suatu fungsi sementara kita menunggu responsnya. Kita dapat menggunakan waktu untuk menjalankan beberapa fungsi lainnya.

Masa depan seperti Promiseobjek dari Javascript. Ini seperti placeholder untuk nilai yang akan terwujud di masa depan. Dalam kasus yang disebutkan di atas, saat menunggu di jaringan I / O, sebuah fungsi dapat memberi kita sebuah container, sebuah janji bahwa ia akan mengisi container dengan nilai saat operasi selesai. Kami berpegang pada objek masa depan dan ketika itu terpenuhi, kami dapat memanggil metode di atasnya untuk mengambil hasil sebenarnya.

Jawaban Langsung: Tidak perlu ensure_futurejika tidak membutuhkan hasil. Mereka bagus jika Anda membutuhkan hasil atau mengambil pengecualian yang terjadi.

Kredit Ekstra: Saya akan memilih run_in_executordan meneruskan sebuah Executorinstance untuk mengontrol jumlah pekerja maksimal.

Penjelasan dan Kode Contoh

Pada contoh pertama, Anda menggunakan coroutine. The waitfungsi mengambil sekelompok coroutines dan menggabungkan mereka bersama-sama. Jadi wait()selesai ketika semua coroutine habis (selesai / selesai mengembalikan semua nilai).

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

The run_until_completeMetode akan memastikan bahwa loop hidup sampai eksekusi selesai. Harap perhatikan bagaimana Anda tidak mendapatkan hasil eksekusi asinkron dalam kasus ini.

Dalam contoh kedua, Anda menggunakan ensure_futurefungsi untuk membungkus coroutine dan mengembalikan Taskobjek yang sejenis Future. Coroutine dijadwalkan untuk dijalankan di loop peristiwa utama saat Anda memanggil ensure_future. Objek masa depan / tugas yang dikembalikan belum memiliki nilai tetapi seiring waktu, saat operasi jaringan selesai, objek masa depan akan menahan hasil operasi.

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

Jadi dalam contoh ini, kami melakukan hal yang sama kecuali kami menggunakan futures daripada hanya menggunakan coroutines.

Mari kita lihat contoh cara menggunakan 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()

Di sini, kami telah menggunakan create_taskmetode pada loopobjek. ensure_futureakan menjadwalkan tugas di loop acara utama. Metode ini memungkinkan kita menjadwalkan coroutine pada loop yang kita pilih.

Kami juga melihat konsep menambahkan callback menggunakan add_done_callbackmetode pada objek tugas.

A Taskadalah donesaat coroutine mengembalikan nilai, memunculkan pengecualian atau dibatalkan. Ada metode untuk memeriksa insiden ini.

Saya telah menulis beberapa posting blog tentang topik ini yang mungkin membantu:

Tentu saja, Anda dapat menemukan detail lebih lanjut di manual resmi: https://docs.python.org/3/library/asyncio.html

masnun
sumber
3
Saya telah memperbarui pertanyaan saya agar sedikit lebih jelas - jika saya tidak memerlukan hasil dari coroutine, apakah saya masih perlu menggunakan ensure_future()? Dan jika saya membutuhkan hasilnya, tidak bisakah saya menggunakan saja run_until_complete(gather(coros))?
rajutan
1
ensure_futuremenjadwalkan coroutine untuk dieksekusi dalam event loop. Jadi saya akan mengatakan ya, itu wajib. Tetapi tentu saja Anda dapat menjadwalkan coroutine menggunakan fungsi / metode lain juga. Ya, Anda dapat menggunakan gather()- tetapi kumpul akan menunggu hingga semua tanggapan terkumpul.
masnun
5
@AbuAshrafMasnun @knite gatherdan waitbenar - benar menggabungkan coroutine yang diberikan sebagai tugas yang menggunakan ensure_future(lihat sumber di sini dan di sini ). Jadi tidak ada gunanya menggunakan ensure_futuresebelumnya, dan itu tidak ada hubungannya dengan mendapatkan hasil atau tidak.
Vincent
8
@AbuAshrafMasnun @knite Juga, ensure_futurememiliki loopargumen, jadi tidak ada alasan untuk menggunakan loop.create_tasklebih ensure_future. Dan run_in_executortidak akan berfungsi dengan coroutine, semaphore harus digunakan sebagai gantinya.
Vincent
2
@vincent ada alasan untuk menggunakan create_tasklebih ensure_future, lihat docs . Kutipancreate_task() (added in Python 3.7) is the preferable way for spawning new tasks.
masi
24

Jawaban sederhana

  • Memanggil fungsi coroutine ( async def) TIDAK menjalankannya. Ini mengembalikan objek coroutine, seperti fungsi generator mengembalikan objek generator.
  • await mengambil nilai dari coroutine, yaitu "memanggil" coroutine
  • eusure_future/create_task jadwalkan coroutine untuk dijalankan pada loop acara pada iterasi berikutnya (meskipun tidak menunggu hingga selesai, seperti untaian daemon).

Beberapa contoh kode

Pertama mari kita jelaskan beberapa istilah:

  • fungsi coroutine, yang Anda async defs;
  • objek coroutine, apa yang Anda dapatkan saat "memanggil" fungsi coroutine;
  • tugas, sebuah objek yang melingkari objek coroutine untuk dijalankan pada event loop.

Kasus 1, awaitdi coroutine

Kami membuat dua coroutine, awaitsatu, dan digunakan create_taskuntuk menjalankan yang lain.

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

Anda akan mendapatkan hasil:

1539486251.7055213 - await
1539486251.7055705 - create_task

Menjelaskan:

task1 dijalankan secara langsung, dan task2 dijalankan dalam iterasi berikut.

Kasus 2, menghasilkan kontrol ke loop peristiwa

Jika kita mengganti fungsi utama, kita bisa melihat hasil yang berbeda:

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

Anda akan mendapatkan hasil:

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

Menjelaskan:

Saat memanggil asyncio.sleep(1), kontrol dikembalikan ke loop peristiwa, dan loop memeriksa tugas yang akan dijalankan, lalu menjalankan tugas yang dibuat oleh create_task.

Perhatikan bahwa, pertama-tama kami memanggil fungsi coroutine, tetapi tidak await, jadi kami hanya membuat satu coroutine, dan tidak membuatnya berjalan. Kemudian, kita memanggil fungsi coroutine lagi, dan membungkusnya dengan create_taskpanggilan, creat_task akan menjadwalkan coroutine untuk dijalankan pada iterasi berikutnya. Jadi hasilnya create taskdieksekusi sebelumnya await.

Sebenarnya, intinya di sini adalah untuk mengembalikan kontrol ke loop, Anda bisa menggunakan asyncio.sleep(0)untuk melihat hasil yang sama.

Dibawah tenda

loop.create_tasksebenarnya menelepon asyncio.tasks.Task(), yang akan menelepon loop.call_soon. Dan loop.call_soonakan memasukkan tugas loop._ready. Selama setiap iterasi loop, ia memeriksa setiap callback di loop._ready dan menjalankannya.

asyncio.wait, asyncio.ensure_futuredan asyncio.gathermenelepon loop.create_tasksecara langsung atau tidak langsung.

Perhatikan juga di dokumen :

Panggilan balik dipanggil sesuai urutan pendaftarannya. Setiap panggilan balik akan dipanggil tepat satu kali.

ospider
sumber
1
Terima kasih atas penjelasan yang bersih! Harus dikatakan, itu desain yang sangat buruk. API tingkat tinggi membocorkan abstraksi tingkat rendah, yang membuat API menjadi lebih rumit.
Boris Burkov
1
lihat proyek curio, yang dirancang dengan baik
ospider
Penjelasan yang bagus! Saya pikir efek dari await task2panggilan tersebut dapat diperjelas. Dalam kedua contoh tersebut, panggilan loop.create_task () adalah yang menjadwalkan tugas2 pada loop peristiwa. Jadi di kedua mantan Anda dapat menghapus await task2dan tetap saja task2 pada akhirnya akan berjalan. Di ex2 perilakunya akan sama, karena await task2saya yakin hanya menjadwalkan tugas yang sudah selesai (yang tidak akan berjalan untuk kedua kalinya), sedangkan di ex1 perilakunya akan sedikit berbeda karena tugas2 tidak akan dijalankan sampai main selesai. Untuk melihat perbedaannya, tambahkan print("end of main")di akhir main ex1
Andrew
10

Sebuah komentar oleh Vincent tertaut ke https://github.com/python/asyncio/blob/master/asyncio/tasks.py#L346 , yang menunjukkan bahwa wait()membungkus coroutine diensure_future() untuk Anda!

Dengan kata lain, kita memang membutuhkan masa depan, dan coroutine akan diam-diam diubah menjadi masa depan.

Saya akan memperbarui jawaban ini ketika saya menemukan penjelasan pasti tentang cara menumpuk coroutines / futures.

rajutan
sumber
Apakah ini berarti bahwa untuk objek coroutine c, await csetara dengan await create_task(c)?
Alexey
3

Dari BDFL [2013]

Tugas

  • Ini adalah coroutine yang dibungkus dengan Future
  • class Task adalah subclass dari class Future
  • Jadi itu bekerja dengan menunggu juga!

  • Apa bedanya dengan coroutine telanjang?
  • Itu bisa membuat kemajuan tanpa menunggu
    • Selama Anda menunggu sesuatu yang lain, yaitu
      • menunggu [sesuatu_lain]

Dengan pemikiran ini, ensure_futuremasuk akal sebagai nama untuk membuat Tugas karena hasil Future akan dihitung apakah Anda menunggunya atau tidak (selama Anda menunggu sesuatu). Ini memungkinkan event loop menyelesaikan Tugas Anda saat Anda menunggu hal lain. Perhatikan bahwa di Python 3.7 create_taskadalah cara yang lebih disukai untuk memastikan masa depan .

Catatan: Saya mengubah "hasil dari" dalam slide Guido menjadi "menunggu" di sini untuk modernitas.

crizCraig.dll
sumber