web-dev-qa-db-ja.com

使用する場合と使用しない場合Python 3.5 `await`?

Python 3.5)でasyncioを使用する流れを取得していますが、awaitingとは何か、およびすべきことの説明がありません。 「これはIO操作であり、したがってawaitedである必要があります」の観点から、最善の判断をしなければなりませんか?

24
dalanmiller

デフォルトでは、すべてのコードは同期です。 async defを使用して関数を非同期で定義し、awaitを使用してこれらの関数を「呼び出す」ことができます。より正確な質問は、「同期ではなく非同期コードをいつ記述すべきか」です。答えは「あなたがそれから利益を得ることができるとき」です。あなたが指摘したように、ほとんどの場合、あなたは利益を得ます。 I/O操作で作業する場合:

# Synchronous way:
download(url1)  # takes 5 sec.
download(url2)  # takes 5 sec.
# Total time: 10 sec.

# Asynchronous way:
await asyncio.gather(
    async_download(url1),  # takes 5 sec. 
    async_download(url2)   # takes 5 sec.
)
# Total time: only 5 sec. (+ little overhead for using asyncio)

もちろん、非同期コードを使用する関数を作成した場合、この関数も非同期である必要があります(async defとして定義する必要があります)。ただし、非同期関数は同期コードを自由に使用できます。何らかの理由なしに同期コードを非同期にキャストしても意味がありません。

# extract_links(url) should be async because it uses async func async_download() inside
async def extract_links(url):  

    # async_download() was created async to get benefit of I/O
    html = await async_download(url)  

    # parse() doesn't work with I/O, there's no sense to make it async
    links = parse(html)  

    return links

非常に重要なことの1つは、長い同期操作(たとえば、50ミリ秒を超える場合、正確に言うのは難しいなど)によって、その時間のすべての非同期操作がフリーズすることです。

async def extract_links(url):
    data = await download(url)
    links = parse(data)
    # if search_in_very_big_file() takes much time to process,
    # all your running async funcs (somewhere else in code) will be frozen
    # you need to avoid this situation
    links_found = search_in_very_big_file(links)

別のプロセスで実行時間の長い同期関数を呼び出すことを回避できます(結果を待機します)。

executor = ProcessPoolExecutor(2)

async def extract_links(url):
    data = await download(url)
    links = parse(data)
    # Now your main process can handle another async functions while separate process running    
    links_found = await loop.run_in_executor(executor, search_in_very_big_file, links)

もう1つの例:asyncioでrequestsを使用する必要がある場合。 requests.getは、同期の長時間実行される関数であり、非同期コード内では呼び出さないでください(これも、フリーズを回避するためです)。しかし、I/Oが原因で長時間実行されています。計算が長いためではありません。その場合、ThreadPoolExecutorの代わりにProcessPoolExecutorを使用して、マルチプロセッシングのオーバーヘッドを回避できます。

executor = ThreadPoolExecutor(2)

async def download(url):
    response = await loop.run_in_executor(executor, requests.get, url)
    return response.text
62

あなたには多くの自由がありません。関数を呼び出す必要がある場合は、これが通常の関数かコルーチンかを調べる必要があります。呼び出す関数がコルーチンである場合に限り、awaitキーワードを使用する必要があります。

async関数が含まれる場合、これらのasync関数を調整する「イベントループ」が存在する必要があります。厳密に言うと、それは必要ではありません。値を送信するasyncメソッドを「手動で」実行できますが、おそらく実行したくないでしょう。イベントループは、まだ完了していないコルーチンを追跡し、次のコルーチンを選択して実行を継続します。 asyncioモジュールはイベントループの実装を提供しますが、これが唯一の可能な実装ではありません。

次の2行のコードを検討してください。

x = get_x()
do_something_else()

そして

x = await aget_x()
do_something_else()

セマンティックはまったく同じです。ある値を生成するメソッドを呼び出し、値が準備できたら変数xに割り当てて、他のことを行います。どちらの場合も、do_something_else関数は、前のコード行が終了した後にのみ呼び出されます。非同期aget_xメソッドの実行前、実行後、または実行中に、コントロールがイベントループに渡されるという意味でもありません。

それでもいくつかの違いがあります:

  • 2番目のスニペットは、別のasync関数内にのみ表示できます
  • aget_x関数は通常ではありませんが、コルーチンです(これはasyncキーワードで宣言されるか、コルーチンとして装飾されます)
  • aget_xは、イベントループと「通信」できます。つまり、いくつかのオブジェクトを生成します。イベントループは、これらのオブジェクトをいくつかの操作を行う要求として解釈できる必要があります(つまり、ネットワーク要求を送信して応答を待つか、またはこのコルーチンをn秒間一時停止するだけです)。通常のget_x関数は、イベントループと通信できません。
1
lesnik