62158


[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 が使える

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


CC BY-SA 4.0
2020-01-10T18:56:53.103
62148
2022-10-20T06:36:05.610
2020-01-11T12:24:31.380
question
python
並列リクエストを行ったほうが遅くなる謎


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

■■■

> 並列リクエストを送るときは必ず 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でかえって時間がかかってしまう件はまだ謎ですが。


CC BY-SA 4.0
2020-01-11T11:03:43.693
62158
2020-01-11T13:12:29.507
2020-01-11T13:12:29.507
26370
26370
62148
answer


回答付いてるけど一応

* * *

#### session について

> session を使わないといけないのでしょうか？

HTTPリクエスト対象のサイトがバラバラか, ほとんど同じサイトかで少し異なり

  * ほぼ同じサイトばかりなら 同じ TCPコネクションを利用して HTTP/1.1 あるいは HTTP/2 で比較的高速にアクセス可能
  * アクセスするサイトがバラバラであれば session使っても使わなくてもほぼ変わりない

以下は 前半がセッション無しの場合。毎回 "Starting new HTTPS connection" が行われてる様子  
後半はセッション使ったことで, 最初の一度しかコネクションが行われていない  
(アクセス数が多くなれば時間差は開くはず)

[code]

    import requests
    import logging
    
    requests_log = logging.getLogger("urllib3")
    requests_log.setLevel(logging.DEBUG)
    
    url = 'https://www.example.com/'
    requests.get(url)
    for n in range(2):
        requests.get(url+ f'page{n}.html')
    
    requests_log.info('=> session版')
    session = requests.Session()
    session.get(url)
    for n in range(2):
        session.get(url+ f'page{n}.html')
    
    #
    DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): www.example.com:443
    DEBUG:urllib3.connectionpool:https://www.example.com:443 "GET / HTTP/1.1" 200 648
    DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): www.example.com:443
    DEBUG:urllib3.connectionpool:https://www.example.com:443 "GET /page0.html HTTP/1.1" 404 648
    DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): www.example.com:443
    DEBUG:urllib3.connectionpool:https://www.example.com:443 "GET /page1.html HTTP/1.1" 404 648
    INFO:urllib3:=> session版
    DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): www.example.com:443
    DEBUG:urllib3.connectionpool:https://www.example.com:443 "GET / HTTP/1.1" 200 648
    DEBUG:urllib3.connectionpool:https://www.example.com:443 "GET /page0.html HTTP/1.1" 404 648
    DEBUG:urllib3.connectionpool:https://www.example.com:443 "GET /page1.html HTTP/1.1" 404 648
    
[/code]

#### 「並列」について

(上の項目と少し説明被るが)  
例えば …  
同一サイトの場合こんな感じ (sessionあり)

[code]

    c: こんにちは
    s: こんにちは
    c: サンドイッチください
    s: サンドイッチどうぞ
    c: コーヒーください
    s: コーヒーどうぞ
    c: さようなら
    s: さようなら
    
[/code]

別サイトの場合

[code]

    c: こんにちは s1
    s1: こんにちは
    c: サンドイッチください
    
    (別の店舗に移動)
    c: こんにちは s2
    s2: こんにちは
    c: コーヒーください
    
    s1: サンドイッチどうぞ
    s2: コーヒーどうぞ
    c: さようなら s1
    s1: さようなら
    c: さようなら s2
    s2: さようなら
    
[/code]

いちばん時間がかかるのは, 注文後から受け取るまで。  
そのため, 別々のサイトへの注文の手順は煩雑だけど, 店舗側(サーバー)が品物を準備し・渡すまで **パラレルに** 行われ高速です (数が多い場合に
一度に大量の品物を受け取れるだけの能力があれば)

剣・盾・鎧・兜 … それぞれを注文する際  
多人数( **Multi** Thread / **Multi** Process) で一斉に店舗回っても, 一人( **シングル**
)でひとつずつ順番に注文しても, 結果は大して変わりません (数日後とか数カ月後とか)

ひとつの店舗の場合も, 多人数で一斉注文しても意味がない (品物用意するのに時間かかるので)  
ただ HTTP/2 以降を利用することで店舗側が準備しやすい方法にでき, 少しだけ高速にできるはず

[code]

    c: こんにちは
    s: こんにちは
    c: 剣ください
    c: 盾ください
    c: 鎧ください
    c: 兜ください
    s: 鎧どうぞ
    s: 剣どうぞ
    s: 盾どうぞ
    s: 兜どうぞ
    c: さようなら
    s: さようなら
    
[/code]

※  
requests は同期的に処理が行われるので, 注文した品を受け取るまで 次の注文ができない  
aiohttp はその点, 次々に注文できるが, (今のところ) HTTP/2 には対応していない

ついでに **セッション無し** の手順も記しておきます

[code]

    (店舗に向かう)
    c: こんにちは
    s: こんにちは
    c: サンドイッチください
    s: サンドイッチどうぞ
    c: さようなら
    s: さようなら
    (店舗に向かう)
    c: こんにちは
    s: こんにちは
    c: コーヒーください
    s: コーヒーどうぞ
    c: さようなら
    s: さようなら
    
[/code]

#### HTTPアクセス時間計測

> 並列化できてるか確認するのに上のようなコードをかいたのですが  
>  (中略)  
>  こんな感じで２回分実行してるとはいえ９倍近く遅くなってます

(質問の環境では)時間計測するにはいくつか問題があります

  * log で見る限り, HTTPアクセス時間というよりも サーバー処理時間(あるいはわざと遅らせてる)としか思えない (なので計測対象を変更すべき) 
    * HTTPリクエスト送信してから HTTPレスポンス・ヘッダー到着まで 5秒
    * (GET 完了まで) 61533 バイトの内容受け取るまで 約 5秒, 全体で 10.8秒
  * VirtualBox 上の環境であること。ホストマシンで何らかの処理を行えば ゲストは処理時間が上乗せされる (なので実機で行うべき)

[code]

    import time
    t0 = time.perf_counter()
    for n in range(2):
        session.get('https://www.rakuten.co.jp/')
    print(f'time: {time.perf_counter() -t0}')
    
    t0 = time.perf_counter()
    r = session.get('https://www.rakuten.co.jp/', stream=True)   # ヘッダーまでは取得
    print(f'time: {time.perf_counter() -t0}')
    
    #
    DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): www.rakuten.co.jp:443
    DEBUG:urllib3.connectionpool:https://www.rakuten.co.jp:443 "GET / HTTP/1.1" 200 None
    DEBUG:urllib3.connectionpool:https://www.rakuten.co.jp:443 "GET / HTTP/1.1" 200 61533
    time: 10.877923578999798
    DEBUG:urllib3.connectionpool:https://www.rakuten.co.jp:443 "GET / HTTP/1.1" 200 61533
    time: 5.029585219000182
    
[/code]

#### asyncio もしくは非同期処理

先の HTTPリクエストから完了までの時間を考慮したとき, (上記での 10秒間) その間にできることがあれば行う のが
(asyncioなどの)非同期処理とも言えます  
(fork なども, fork自体は別の機能だが結果的に非同期処理とか)

generator (`yield` 使うやつ) にも似て (そちらもコルーチンと呼ばれるが, coroutine object とは別),  
関数呼び出しのサブルーチンなどとは異なり 制御が (状況に応じて)移っていくもの。  
(解釈中の(実行中の)位置が移る)

`await` に到達し 未完であれば制御を返す。すると, 別な非同期関数の先頭もしくは (完了していれば)その非同期関数の続き
(`await`の続き)から再開する(制御が渡る)

ただし, asyncio では(今のところ) TCPや UDP などしか手順が用意されておらず (非同期処理の仕組みとしては, 完了通知は TCP UDP
の他にも多種存在する),  
その手順に沿っていないものは Threadの完了をもって「完了」として扱うほかない, ということです

> with やメソッドがネストしててかえってわかりにくいので

慣れが必要でしょう。asyncioに慣れていけば分かることと思われます


CC BY-SA 4.0
2022-10-15T08:55:55.333
91609
2022-10-20T06:36:05.610
2022-10-20T06:36:05.610
43025
43025
62148
answer