質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.35%
並列処理

複数の計算が同時に実行される手法

非同期処理

非同期処理とは一部のコードを別々のスレッドで実行させる手法です。アプリケーションのパフォーマンスを向上させる目的でこの手法を用います。

Python

Pythonは、コードの読みやすさが特徴的なプログラミング言語の1つです。 強い型付け、動的型付けに対応しており、後方互換性がないバージョン2系とバージョン3系が使用されています。 商用製品の開発にも無料で使用でき、OSだけでなく仮想環境にも対応。Unicodeによる文字列操作をサポートしているため、日本語処理も標準で可能です。

Q&A

解決済

1回答

1752閲覧

Python asyncio/awaitの挙動について

koyamashinji

総合スコア45

並列処理

複数の計算が同時に実行される手法

非同期処理

非同期処理とは一部のコードを別々のスレッドで実行させる手法です。アプリケーションのパフォーマンスを向上させる目的でこの手法を用います。

Python

Pythonは、コードの読みやすさが特徴的なプログラミング言語の1つです。 強い型付け、動的型付けに対応しており、後方互換性がないバージョン2系とバージョン3系が使用されています。 商用製品の開発にも無料で使用でき、OSだけでなく仮想環境にも対応。Unicodeによる文字列操作をサポートしているため、日本語処理も標準で可能です。

0グッド

2クリップ

投稿2021/06/15 01:17

編集2021/06/15 01:33

Pythonでの非同期処理コード実装を学んでいます(沼にはまっています)。

awaitcreate_taskasyncio.sleep()等の挙動はひととおり検証しましたが、
以下コードの挙動がどうしても説明できません。

わからないこと

どうしてtask3task4よりも先に実行されるのか。

自分は以下の理解をしています。

main処理を呼び出し.
task3task4をクリエイト. 両方、待機状態.
awaittask4が実行される.
awaittask3が実行される.

baz()bar()内で、制御をmainに戻し他のPENDINGタスクを実行するコード(asyncio.sleep等)も特に定義していないのに
なぜtask3に先に制御が移っている・・・?)

python

1import asyncio 2 3async def main(): 4 print("start") 5 task3 = asyncio.create_task(foo("task3")) # task3をクリエイト. PENDING. 6 task4 = await bar() # bar()の処理を実行. task4をクリエイト. PENDING. 7 8 await task4 # task4を実行. 9 await task3 # task3を実行. 10 11 print("finish") 12 13 14async def foo(text): 15 print(text) 16 17 18async def baz(text): 19 print(text) 20 21 22async def bar(): 23 task4 = asyncio.create_task(baz("task4")) 24 print("bar") 25 return task4 26 27 28asyncio.run(main())

console

1start 2bar 3task3 4task4 5finish


(何が問題なのかよく分からず, 本件の本質がとらえきれてないので 質問文も曖昧になっていますが、
分かり次第、より明確に書き換える予定)

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答1

0

ベストアンサー

  • task3 の foo 内に await が無い為、main側でawait した時点でタスクが作成順に実行されている。
  • bar() 関数は task4 を返すので bar() は task4 を返すコルーチン
    task4 = await bar() の右辺を評価した時点で タスクは実行されてます。

python

1task4 = await bar() # bar()の処理を実行. task4をクリエイト. PENDING.

検証: 以降の await task4 を追加したり削除しても結果は変わりません。
この時点の task4 は消化済みのタスクなので、何もしないで次の処理に移ります。

訂正: タスクの実行を検証

diff

1- await task4 # task4を実行. 2- await task3 # task3を実行. 3+ await asyncio.sleep(0) # task3, task4 が実行。await の右辺は何でもよい 4 # 実行コンテキストの切り替え、処理をイベントループに戻す 5 # → 実行可能なタスクが処理される 6

追記: 想定されている挙動 task3 を task4 後に遅らせたい場合は、
恐らく、タスクではなくコルーチン

diff

1- task3 = asyncio.create_task(foo("task3")) 2+ task3 = foo("task3")

※ この場合、変数名に task は適切でないので、別名に。


どうしてtask3がtask4よりも先に実行されるのか。

タスクは、作成時に イベントループにスケジュールされ、
await でイベントループに処理が戻ったタイミングで、実行可能ならば実行されます。

foo,bar,baz のコルーチン内に await がない ということは、どれも 即実行可能なので、
その為、await 順ではなく、実行順序は作成順になります。


  • create_task() で Task() が作成され、
  • タスクはコンストラクタ内でイベントループにスケジュールされます。

投稿2021/06/15 10:25

編集2021/06/15 12:18
teamikl

総合スコア8760

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

koyamashinji

2021/06/15 11:30

回答ありがとうございます。 task4 = await bar() の時点でtask4が実行される(消化済みである)のなら、なぜその時点で"task4"が出力されずに "task3"が出力された後になるのでしょうか。 まだ少し混乱しています。
teamikl

2021/06/15 11:40 編集

何度か回答を編集しました。 コメントに対する解は、タスクは作成された時点で開始をスケジュールされるので、 次にawaitに到達した時点で task3 が先に実行されるからです。 (この await は、await task3 である必要はありません) await ~は、~の完了を待つ。なので、開始タイミングと一致するとは限りません。
退会済みユーザー

退会済みユーザー

2021/06/15 11:45 編集

(teamikl様、いつもGUI関係を中心に深い知識と慧眼のある御回答で尊敬しております) 横からすみません。消化済みなのは、task4ではなくtask3ではないでしょうか。 コメント欄にソースを書いて恐縮ですが、(2行目以降は同じ1段下げインデントです) main関数を ``` async def main(): print("start") task3 = asyncio.create_task(foo("task3")) print("before await bar()") task4 = await bar() print("before await task4") await task4 print("after await task4") await task3 print("finish") ``` とした場合、 実行結果は ``` start before await bar() bar before await task4 task3 task4 after await task4 finish ``` となると思います。 すなわち「before await task4」 のあとに task3 task4 が続いています。 仮にtask4が「消化済み」であるならば、「task4」という表示は、少なくとも、「before await task4」の前に来るべきではないでしょうか? そうなっていないということは、 task4 = await bar() の時点ではtask4はイベントループにセットされただけで、次の await task4の部分で(task3の実行後に)実行されている、ということにならないでしょうか。  (自分の「消化済み」という言葉の理解に行き違いがある可能性もありますが) 上述の通り、task3はtask4より前に完了しているので、 await task4 の後の await task3こそが、何もしない消化済みのタスクといえると思います。 勘違いや知識不足でしたら恐縮ですが、自分の考えが間違っているか確認したく、よろしくお願いいたします。
teamikl

2021/06/15 12:20 編集

すみません、回答を後から修正してしまって、幾つか確認漏れがあるかもしれません。 最初に投稿した回答には見落としがありました。 >右辺を評価した時点で タスクは実行されてます。 の説明は間違いで、タスクが実行(printで表示)されるのは、その次の await でした。 (回答欄も後で取消線で訂正します) タスクが実行されるのは、foo,baz 内に await が無い為、同じ await 内で、 実行される順序自体はタスクが生成された順番になります。
teamikl

2021/06/15 12:11

qnoir様、コメント指摘ありがとうございます。 asyncio も、GUIと同様のイベント駆動なので、関心のあるトピックです。 >task4 = await bar() の時点ではtask4はイベントループにセットされただけで、次の >await task4の部分で(task3の実行後に)実行されている、ということにならないでしょうか。 この通りの説明であってます。
退会済みユーザー

退会済みユーザー

2021/06/15 12:20

御確認ありがとうございます。
teamikl

2021/06/15 12:43

回答を訂正・編集しました。 await task3 や await task4 が誤読の元だったかもしれないですね。 - タスク内のコルーチンには await が無いので、処理待ちするようなコルーチンがない。 - 処理をイベントループに戻せればタスクは実行されるので、  タスクの実行を期待するだけなら、await の右は awaitable なら何でもよい。 ==== 補足で、確認を取った点のメモ asyncioライブラリ内部のソースコードの追いかけ。 # create_task() 内で Task 生成 https://github.com/python/cpython/blob/8ebd9447e9618240ee3b955888d114376f64117b/Lib/asyncio/base_events.py#L426 # class Task: コンストラクタ内 loop.call_soon( ... ) でイベントループにセット https://github.com/python/cpython/blob/8ebd9447e9618240ee3b955888d114376f64117b/Lib/asyncio/tasks.py#L112 # loop.call_soon の説明 https://docs.python.org/ja/3/library/asyncio-eventloop.html#asyncio.loop.call_soon > Callbacks are called登録された in the order in which they are registered. 登録順に実行 NOTE: 登録されたタスクが実行されるのは、イベントループに処理が戻る await のタイミング。
koyamashinji

2021/06/15 13:14

丁寧な回答誠に感謝致します。 await ~ は、一旦イベントループに制御を戻す。イベントループ内で処理待ちになっているtaskオブジェクトを実行する、と理解しました。 その場合、task4 = await bar()の部分のawaitで、一旦イベントループに制御が戻り、既にセットされているtask3が実行された後、"bar"がプリントされるようにも思いますが、なぜそうならないのでしょうか・・ 理解が悪く恐縮です。
teamikl

2021/06/15 13:32

bar() 内に await がなく、return してるので直ぐにmain側に処理が戻り イベントループに処理が完全に戻っていないのだと思います。 awaitable ならなんでも~は、正確な説明ではなかったですね。
bsdfan

2021/06/15 13:50

awaitする対象が、コルーチンの場合は処理は止まりませんが、タスクやFutureの場合は一時停止するという違いがあります。
koyamashinji

2021/06/16 07:30

皆様、有難うございます。 皆さんから頂いた回答及びコメントを踏まえ、以下のように説明してみました。 おそらく、私の混乱の原因の根本は await コルーチン、await タスクの挙動の違いだと思っています。(【※1】【※2】の部分) (理解が及んでない点ありましたらご助言頂けると幸いです。) -------------------------------------------------------- asyncio.runで、イベントループが開始される。 制御がmainに移る。 mainは、"start"を出力する。 mainは、コルーチンfooのタスク(task3)を作成。task3はイベントループにて待機。 mainは、コルーチンbarをawaitする。【※1】 制御がコルーチンbarに移る。 コルーチンbarは、"bar"を出力する。 コルーチンbarは、コルーチンbazのタスク(task4)を作成する。task4はイベントループにて待機。 コルーチンbarは、task4を返す。 制御がmainに戻る。 mainは、task4をawaitする。 【※2】 制御がイベントループに移る。 イベントループは、待機中のtask3、task4を実行する。(タスクは作成順に実行される) 待機中のタスクがなくなったら、制御がmainに戻る。 mainは、task3をawaitする。 制御がイベントループに移る。 イベントループに待機中のタスクがないので、制御がmainに戻る。 mainは、"finish"を出力する。 イベントループが終了する。 【※1】 awaitする対象が、コルーチンの場合 :制御はイベントループに移らず、コルーチンの処理が実行される。 【※2】 awaitする対象が、タスクやFutureの場合:制御はイベントループに移り、待機中のタスクが作成順に実行される。 --------------------------------------------------------
teamikl

2021/06/16 12:05

await ~は、~の完了を待つという共通の操作です。 多分、誤解されてるかなと思う要点は、「実行開始のタイミング」 `await コルーチン` は、そこでコルーチンが開始・実行されますが、 `await タスクは`、そこがタスク実行開始のタイミングとは限りません。 場合によっては開始時と一致することも有りますが、 それ以前に await で実行できるタイミングがあれば、そこで消化されます。 >【※1】awaitする対象が、コルーチンの場合 :制御はイベントループに移らず、コルーチンの処理が実行される。 await する対象が要点ですが、コルーチンかタスク他という区別ではなく 内部で await しているかどうか、またawait していても終端(?)が await になっているかどうかで検証して見て下さい。(→ イベントループに処理が戻るかどうか) 適切な用語が見当たらなかったので、コードで提示すると # NOTE: ドキュメントでは、3つとも awaitable & コルーチンの定義内です。 async def test(): # await がない→コルーチンの実行を中断するポイントがない   return async def test2():   await test() # await を呼んでいるが、test() で行き止まり async def test3(): # OK   await asyncio.sleep(0) await ~の対象(async def bar())の 「内部で」 await してないのが相違点なので、 通常の非同期での利用であれば、await でイベントループに処理を戻すという認識でほぼ大丈夫です。 コルーチンの「内部に」await があれば、期待通り制御がイベントループに移ります。
koyamashinji

2021/06/17 04:58

かなり理解が進みました。ありがとうございます。 昨年プログラミングを始めて以来、本件で一番深い沼にはまっておりましたが、体半分引き上げてもらった感覚です。asyncioは私にとってかなり難解なので、これからも検証を続けていきたいと思います。取り急ぎお礼まで。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.35%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問