並列リクエストを行ったほうが遅くなる謎


[python の async await の使い方：'await' outside
function](https://ja.stackoverflow.com/questions/62081/python-%e3%81%ae-async-
await-%e3%81%ae%e4%bd%bf%e3%81%84%e6%96%b9-await-outside-function)

こちらで async await について質問したんですが内容が多くなってしまったので  
トピックを絞って再度質問したいと思います

[code]

    import aiohttp
    import asyncio
    
    
    async def fetch(session, url):
        async with session.get(url) as response:
            print(await response.text())
    
    
    async def main():
        async with aiohttp.ClientSession() as session:
            await asyncio.gather(
                fetch(session, 'https://www.rakuten.co.jp/'),
                fetch(session, 'https://www.amazon.co.jp/'),
            )
    
    asyncio.run(main())
    
[/code]

という http request のサンプルコードを教えていただいたのですが、  
並列リクエストを送るときは必ず session を使わないといけないのでしょうか？  
ただの request ではなぜダメなんでしょうか

fork join でかく並列化しかさわったことがなくて  
async await の仕組みや書き方が全く理解できないので  
使わなくても済むならもっと単純なコードに落としたいです

あとコード量は少ないのかもしれないですが  
with やメソッドがネストしててかえってわかりにくいので  
フラットな手続き型のコードで async, await を理解できるようなサンプルはないでしょうか

親プロセスと子プロセスがコードをどの順でどう実行されるかが全く分からないです

* * *

追記

[code]

    import aiohttp
    import asyncio
    import time
    import requests
    
    async def fetch(session, url):
        async with session.get(url) as response:
            print((await response.text())[0:10])
    
    async def main():
        async with aiohttp.ClientSession() as session:
            await asyncio.gather(
                fetch(session, 'https://www.rakuten.co.jp/'),
                fetch(session, 'https://www.rakuten.co.jp/'),
            )
    
    start  = time.time()
    response = requests.get('https://www.rakuten.co.jp/')
    print(response.text[0:10])
    response = requests.get('https://www.rakuten.co.jp/')
    print(response.text[0:10])
    end  = time.time()
    print(end - start)
    
    start  = time.time()
    asyncio.run(main())
    end  = time.time()
    print(end - start)
    
[/code]

並列化できてるか確認するのに上のようなコードをかいたのですが

[code]

    <!DOCTYPE 
    <!DOCTYPE 
    10.279092073440552
    <!DOCTYPE 
    <!DOCTYPE 
    44.95866298675537
    
[/code]

こんな感じで２回分実行してるとはいえ９倍近く遅くなってます  
（ステータスコードの取得方法がわからないので  
ネットワークでイレギュラーなことが起こってないか  
確認のためレスポンスの１０文字だけ表示してます）

これって本当に正しく並列化されてるんでしょうか

環境：

Windows10 上の VirtualBox 上の ubuntu + Python 3.7.5 です

* * *

今日1日ドキュメントを読んでようやくわかってきたんですが、  
async, await はアプリケーションレベルで疑似的なマルチスレッドを実現してるだけで  
OSレベルのマルチスレッド化を行ってるわけじゃないんですね…

async await とかくことでOSのシステムコールレベルで  
どういうコードが生成されてるのか疑問だったのですが…

ここからはまだ理解がおぼつかないのですが

async 宣言することで戻り値を遅延評価するしくみが自動で追加されて  
それを await で呼ぶことで値が入ってたらその値を取得して  
まだはいってなかったら別の処理を実行させるみたいな感じで  
await = join と yield をまぜたような感じになるんでしょうか

なので async を使うためには中に await がかかれてある専用のライブラリじゃないと  
そもそも処理を手放さないので非同期に実行できなくて  
asyncio.sleep とか aiohttp とかが必要になる

で aiohttp のメソッドに session が必要なものしか用意されてなくて  
非同期処理とは関係ないけど session を使わないと  
python ではシングルスレッド非同期が実現できず

thread レベルで並列化する run_in_execute っていうのを使うとただの requests が使える

という理解で大丈夫でしょうか？




いくつかの特徴的なトピック毎に回答を書きます。

■■■

> 並列リクエストを送るときは必ず session を使わないといけないのでしょうか？  
>  ただの request ではなぜダメなんでしょうか

ここは`session`というよりは`aiohttp`のことですね。

いくつかの記事を見ると、`requests`はノンブロッキングではなくブロッキング動作をするとあって、それが原因でasync/awaitと組み合わせても効果が無いようです。  
そして大概`aiohttp`を使いましょう、と書いてあります。  
[pythonで複数のhttp
requestを同時に投げる方法(requests)について](https://pod.hatenablog.com/entry/2018/09/22/215030)

> いわゆるpythonでのhttp requestのデファクトスタンダードは[requests](http://docs.python-
> requests.org/en/master/)だと思う。ところでこのrequestsはnon
> blockingではないのでasyncio上で使おうとすると処理がblockしてしまう。

[How to run requests.get asynchronously in Python 3 using
asyncio?](https://stackoverflow.com/q/48196172/9014308)

> `requests.get` is blocking by nature.  
>  You should either find async alternative for requests like `aiohttp`
> module:

[How to run asynchronous web requests in parallel with Python 3.5 (without
aiohttp)](https://hackernoon.com/how-to-run-asynchronous-web-requests-in-
parallel-with-python-3-5-without-aiohttp-264dc0f8546)

> **Disclaimer:** If you have a higher version of Python available (3.5.2+), I
> highly recommend using aiohttp instead. It’s an incredibly robust library
> and a great solution for this kind of problem.

* * *

■■■

`asyncio`でも`requests`は使えるけれど、それはasync/awaitではなくて`run_in_executer`とかで別スレッドとして実行するやり方のようですね。  
[pythonで複数のhttp
requestを同時に投げる方法(requests)について](https://pod.hatenablog.com/entry/2018/09/22/215030)

> **run_in_executor()を使う場合**  
>
> 今度は`run_in_executor()`を使った場合を見ていく。`run_in_executor()`の第一引数にNoneを渡すとデフォルトのexecutorで呼ばれる(設定を変更していない限りconcurrent.futures.ThreadPoolExecutorで実行される)。

[How to run requests.get asynchronously in Python 3 using
asyncio?](https://stackoverflow.com/q/48196172/9014308)

> or run `requests.get` in separate thread and await this thread
> asynchronicity using `loop.run_in_executor`:
[code]

>     executor = ThreadPoolExecutor(2)
>  
>     async def get(url):
>         response = await loop.run_in_executor(executor, requests.get, url)
>         return response.text
>  
[/code]

[Multithread python requests](https://stackoverflow.com/q/38671803/9014308)  
回答に記載されている：長いので引用は省略  
`asyncio`ではなく`threading.Thread()`でスレッド作成・実行している

`requests`を使うにしても色々と周辺の追加ロジックが必要で、 **「使わなくても済むならもっと単純なコードに落としたいです」**
は実現出来そうに無いですね。

* * *

■■■

async/awaitはプロセスでもスレッドでも無いコルーチンというものをサポートしています。  
[asyncio --- 非同期 I/O](https://docs.python.org/ja/3.7/library/asyncio.html)  
[コルーチンと Task](https://docs.python.org/ja/3.7/library/asyncio-task.html)

「フラットな手続き型のコードで async, await を理解できるようなサンプルはないでしょうか」という要望にはこたえられませんが、Python
3.5.4とか3.6.10のドキュメントにある図が理解しやすいかもしれません。  
[18.5.3.1.3. 例: コルーチンのチェーン - Python 3.6.10
ドキュメント](https://docs.python.org/ja/3.6/library/asyncio-task.html#example-
chain-coroutines)  
[18.5.3.1.3. 例: コルーチンのチェーン - Python 3.5.4
ドキュメント](https://docs.python.org/ja/3.5/library/asyncio-task.html#example-
chain-coroutines)

だから「親プロセスと子プロセスがコードをどの順でどう実行されるかが全く分からないです」という疑問は、対象の名前からして理解不足でしたね。  
上記の図で雰囲気程度は頭に入るのではないでしょうか。

* * *

■■■

「追記...これって本当に正しく並列化されてるんでしょうか」については、まあ並列化はされていないでしょうね。

ただ追記の確認プログラムをWindows10環境で実行すると、最初の2つ連続が10.30x秒、次のasync.runが5.08x秒になりました。(コメントに書いた時は連続部分はURLが1つだけ記載で5.08x～5.14x秒でした)

おそらくVirtualBoxのVMで動作させていることか、その関連の設定内容に原因がありそうです。その辺の動作環境が整えば、それなりに動作してくれるでしょう。

@Fumu 7 さんのコメントとか、Pythonドキュメントに書かれたように、Pythonプログラム自身はasync/awaitでは並列動作はしていません。

しかし、正しい動作環境になれば、Webサイトへの依頼はサイトからの応答完了を待たずに複数出すことが出来るので、全体的な動きとしては並列動作しているように見せることが出来ます。

* * *

質問の最後に追記された読解内容は、おそらく合っているでしょう。  
VMでかえって時間がかかってしまう件はまだ謎ですが。




いくつかの特徴的なトピック毎に回答を書きます。

■■■

> 並列リクエストを送るときは必ず session を使わないといけないのでしょうか？  
>  ただの request ではなぜダメなんでしょうか

ここは`session`というよりは`aiohttp`のことですね。

いくつかの記事を見ると、`requests`はノンブロッキングではなくブロッキング動作をするとあって、それが原因でasync/awaitと組み合わせても効果が無いようです。  
そして大概`aiohttp`を使いましょう、と書いてあります。  
[pythonで複数のhttp
requestを同時に投げる方法(requests)について](https://pod.hatenablog.com/entry/2018/09/22/215030)

> いわゆるpythonでのhttp requestのデファクトスタンダードは[requests](http://docs.python-
> requests.org/en/master/)だと思う。ところでこのrequestsはnon
> blockingではないのでasyncio上で使おうとすると処理がblockしてしまう。

[How to run requests.get asynchronously in Python 3 using
asyncio?](https://stackoverflow.com/q/48196172/9014308)

> `requests.get` is blocking by nature.  
>  You should either find async alternative for requests like `aiohttp`
> module:

[How to run asynchronous web requests in parallel with Python 3.5 (without
aiohttp)](https://hackernoon.com/how-to-run-asynchronous-web-requests-in-
parallel-with-python-3-5-without-aiohttp-264dc0f8546)

> **Disclaimer:** If you have a higher version of Python available (3.5.2+), I
> highly recommend using aiohttp instead. It’s an incredibly robust library
> and a great solution for this kind of problem.

* * *

■■■

`asyncio`でも`requests`は使えるけれど、それはasync/awaitではなくて`run_in_executer`とかで別スレッドとして実行するやり方のようですね。  
[pythonで複数のhttp
requestを同時に投げる方法(requests)について](https://pod.hatenablog.com/entry/2018/09/22/215030)

> **run_in_executor()を使う場合**  
>
> 今度は`run_in_executor()`を使った場合を見ていく。`run_in_executor()`の第一引数にNoneを渡すとデフォルトのexecutorで呼ばれる(設定を変更していない限りconcurrent.futures.ThreadPoolExecutorで実行される)。

[How to run requests.get asynchronously in Python 3 using
asyncio?](https://stackoverflow.com/q/48196172/9014308)

> or run `requests.get` in separate thread and await this thread
> asynchronicity using `loop.run_in_executor`:
[code]

>     executor = ThreadPoolExecutor(2)
>  
>     async def get(url):
>         response = await loop.run_in_executor(executor, requests.get, url)
>         return response.text
>  
[/code]

[Multithread python requests](https://stackoverflow.com/q/38671803/9014308)  
回答に記載されている：長いので引用は省略  
`asyncio`ではなく`threading.Thread()`でスレッド作成・実行している

`requests`を使うにしても色々と周辺の追加ロジックが必要で、 **「使わなくても済むならもっと単純なコードに落としたいです」**
は実現出来そうに無いですね。

* * *

■■■

async/awaitはプロセスでもスレッドでも無いコルーチンというものをサポートしています。  
[asyncio --- 非同期 I/O](https://docs.python.org/ja/3.7/library/asyncio.html)  
[コルーチンと Task](https://docs.python.org/ja/3.7/library/asyncio-task.html)

「フラットな手続き型のコードで async, await を理解できるようなサンプルはないでしょうか」という要望にはこたえられませんが、Python
3.5.4とか3.6.10のドキュメントにある図が理解しやすいかもしれません。  
[18.5.3.1.3. 例: コルーチンのチェーン - Python 3.6.10
ドキュメント](https://docs.python.org/ja/3.6/library/asyncio-task.html#example-
chain-coroutines)  
[18.5.3.1.3. 例: コルーチンのチェーン - Python 3.5.4
ドキュメント](https://docs.python.org/ja/3.5/library/asyncio-task.html#example-
chain-coroutines)

だから「親プロセスと子プロセスがコードをどの順でどう実行されるかが全く分からないです」という疑問は、対象の名前からして理解不足でしたね。  
上記の図で雰囲気程度は頭に入るのではないでしょうか。

* * *

■■■

「追記...これって本当に正しく並列化されてるんでしょうか」については、まあ並列化はされていないでしょうね。

ただ追記の確認プログラムをWindows10環境で実行すると、最初の2つ連続が10.30x秒、次のasync.runが5.08x秒になりました。(コメントに書いた時は連続部分はURLが1つだけ記載で5.08x～5.14x秒でした)

おそらくVirtualBoxのVMで動作させていることか、その関連の設定内容に原因がありそうです。その辺の動作環境が整えば、それなりに動作してくれるでしょう。

@Fumu 7 さんのコメントとか、Pythonドキュメントに書かれたように、Pythonプログラム自身はasync/awaitでは並列動作はしていません。

しかし、正しい動作環境になれば、Webサイトへの依頼はサイトからの応答完了を待たずに複数出すことが出来るので、全体的な動きとしては並列動作しているように見せることが出来ます。

* * *

質問の最後に追記された読解内容は、おそらく合っているでしょう。  
VMでかえって時間がかかってしまう件はまだ謎ですが。

