1. maueki

    No comment

    maueki
Changes in body
Source | HTML | Preview
@@ -1,158 +1,248 @@
# TL;DR
`await coroutine`と`await future`の違いが理解できていればおおよそOK
# はじめに
-python3.5から導入されたasync/awaitだが入門記事を読んでもいまいちピンとこない。結局のところ[公式ドキュメント](https://docs.python.jp/3/library/asyncio.html)読むのが一番なのだが、頭から読むには分量が多すぎる。
+python3.5から導入されたasync/awaitだが入門記事を読んでもいまいちピンとこない。結局のところ[公式ドキュメント](https://docs.python.jp/3/library/asyncio.html)読むのが一番なのだが、頭から読むには分量が多すぎる。
そこで公式ドキュメントを読む前にこれを抑えておけば勘所がつかめるというものを書いてみた。
`yield`や`yield from`については知っていると理解が早いのだが、この記事ではそれらの知識は前提としないように書いた。
+またasync/awaitはマルチスレッドと密接に関係していると思っている人もいるかもしれないが、pythonのasync/awaitは勝手にスレッドを生成したりは一切しない。この記事にあるコードも特に断りがない限りシングルスレッドで動作する。
+
## `async def`はコルーチン関数定義
まずは`async`を使ってみる
```py:hello.py
import asyncio
async def hello_world():
print("Hello World!")
hello_world()
```
```bash
$ python3 hello.py
hello.py:6: RuntimeWarning: coroutine 'hello_world' was never awaited
hello_world()
```
実行すると「'hello_world'コルーチンがawaitされてないよ」という警告がでる。
C#のasync/awaitに馴染んでる人だと「?」となるのではないだろうか。C#の場合は`async`がついていてもただの関数に過ぎないため、同じノリで考えると"Hello World!"は出力されそうだ。
しかしpythonの`async def`は単なる関数定義ではなく[コルーチン関数定義](https://docs.python.org/ja/3/reference/compound_stmts.html#coroutine-function-definition)であり、実行するとコルーチンオブジェクトが返る。
コルーチンオブジェクトはイベントループ内でのみ実行が可能であり、上記のようなコードを書くと警告が出てしまう。
実際にhello_worldコルーチンをイベントループで実行してみたのが以下のコード
```py:hello.py
import asyncio
async def hello_world():
print("Hello World!")
loop = asyncio.get_event_loop()
loop.run_until_complete(hello_world())
```
```bash
$ python3 hello.py
Hello World!
```
# `await`を使ってみる
+```python
+import asyncio
+
+async def hello_world():
+ print("Hello World!")
+
+async def call_hello_world():
+ await hello_world()
+
+loop = asyncio.get_event_loop()
+loop.run_until_complete(call_hello_world())
+```
+
+```bash
+$ python3 hello.py
+Hello World!
+```
+
+`await`を使って`hello_world()`が生成したコルーチンオブジェクトが実行されているように見える。
+
+次の例はどうか
+
+```python
+import asyncio
+import time
+
+async def hello_world(n):
+ time.sleep(1)
+ print("{}: Hello World!".format(n))
+
+async def call_hello_world1():
+ print("call_hello_world1()")
+ await hello_world(1)
+
+async def call_hello_world2():
+ print("call_hello_world2()")
+ await hello_world(2)
+
+loop = asyncio.get_event_loop()
+loop.create_task(call_hello_world1())
+loop.run_until_complete(call_hello_world2())
+```
+
+```bash
+$ python3 hello.py
+call_hello_world1()
+# 1秒待つ
+1: Hello World!
+call_hello_world2()
+# 1秒待つ
+2: Hello World!
+```
+
+以下のように出力されるのでは?と思った人もいるだろう
+
+```bash
+call_hello_world1()
+call_hello_world2()
+# 1秒待つ
+1: Hello World!
+2: Hello World!
+```
+
+上記のように出力してほしければ以下のようなコードになる。
+
+```python
+import asyncio
+
+async def hello_world(n):
+ await asyncio.sleep(1)
+ print("{}: Hello World!".format(n))
+
+async def call_hello_world1():
+ print("call_hello_world1()")
+ await hello_world(1)
+
+async def call_hello_world2():
+ print("call_hello_world2()")
+ await hello_world(2)
+
+loop = asyncio.get_event_loop()
+loop.create_task(call_hello_world1())
+loop.run_until_complete(call_hello_world2())
+```
+
+先ほどとの違いは`time.sleep(1)`が`await asyncio.sleep(1)`に変わっていることだ。
+自分はこのコードに遭遇したとき「`await hello_world()`は`hello_world()`が関数の様に実行されるのに対して同じく`await`を使っている`await asyncio.sleep(1)`は制御がイベントループに戻っている???」
+と大変混乱した。ここらへんの挙動の違いを以下で説明したい。
# `await future`と`await coroutine`
-[18.5.3.1. コルーチン](https://docs.python.org/ja/3/library/asyncio-task.html#coroutines)を読んでいると気になる記述に出くわす。
+ところで[18.5.3.1. コルーチン](https://docs.python.org/ja/3/library/asyncio-task.html#coroutines)を読んでいると気になる記述に出くわす。
> コルーチンができること:
>
> * `result = await future` or `result = yield from future` -- suspends the coroutine until the future is done, then returns the future's result, or raises an exception, which will be propagated. (If the future is cancelled, it will raise a CancelledError exception.) Note that tasks are futures, and everything said about futures also applies to tasks.
> * `result = await coroutine` or `result = yield from coroutine` -- wait for another coroutine to produce a result (or raise an exception, which will be propagated). The coroutine expression must be a call to another coroutine.
どうやらpythonの`await`は`future`と`coroutine`の2つが取れるらしいが、挙動が微妙に違う。
`await future`の方はsuspends、つまり制御を手放すのに対して`await coroutine`の方は単にcoroutineの終了を待つだけに見える。
ためしに以下のようなコードを書いてみる。
```python:await_sample.py
import asyncio
import time
async def task_one():
print("task_one: before sleep")
await asyncio.sleep(0.1)
print("task_one: after sleep")
return 1
async def time_sleep():
time.sleep(5)
print("time_sleep")
async def task_two():
print("task_two: before task_one")
await time_sleep()
print("task_two: after task_one")
return 2
async def test(loop):
t1 = loop.create_task(task_one())
t2 = loop.create_task(task_two())
print(repr(await t1))
print(repr(await t2))
def main():
loop = asyncio.get_event_loop()
try:
loop.run_until_complete(test(loop))
finally:
loop.close()
main()
```
```bash
$ python3 await_sample.py
task_one: before sleep
task_two: before task_one
# ここで5秒待つ
time_sleep
task_two: after task_one
task_one: after sleep
1
2
```
挙動としては、一見以下のように見える(が、間違っている)
1. `task_one()`が実行され、`await asyncio.sleep(0.1)`で制御を手放す
2. `task_two()`が開始されるが`await time_sleep()`では制御を手放さずそのまま寝て返ってくる
`asyncio.sleep()`は[コルーチンである](https://docs.python.org/ja/3/library/asyncio-task.html#asyncio.sleep)。また、`time_sleep()`もコルーチンのはずである。片方は制御を手放し、片方は制御を手放さない。これはどういうことだろうか?
ポイントは`asyncio.sleep()`の実装にある。
```python:cpython/Lib/asyncio/tasks.py
async def sleep(delay, result=None, *, loop=None):
"""Coroutine that completes after a given time (in seconds)."""
if delay <= 0:
await __sleep0()
return result
if loop is None:
loop = events.get_event_loop()
future = loop.create_future()
h = loop.call_later(delay,
futures._set_result_unless_cancelled,
future, result)
try:
return await future
finally:
h.cancel()
```
`return await future`があるのが見て取れる。
冒頭の記述を思い出して解釈するとサンプルコードの挙動は以下のようになる。
1. `task_one()`が実行され、`await asyncio.sleep(0.1)`によりコルーチンである`asyncio.sleep()`内部が実行される
2. `asyncio.sleep()`内部の`await future`で制御を手放す
3. `task_two()`が開始され`await time_sleep()`により、コルーチンである`time_sleep()`内部が実行される
4. `time_sleep()`内には制御を手放す構文はないため`time.sleep(5)`ごとそのまま実行され呼び出し元に返る