Python3.5で実装されたasync/awaitを使って軽量スレッドの性能ベンチマーク


Python3.5でasync/awaitが追加されていたのでメモリ消費量とコンテキストスイッチのコストの観点でベンチマークを取ってみました。

# async/await構文とは
非同期処理やノンブロッキングI/O処理を良い感じに書ける非同期処理のパラダイムにおける最先鋭の構文です。C#に実装されたあと、C++,VB,Node.jsに実装されついにPythonにもやってきた！という感じです。特徴はいままでThreadingで頑張って書いてた非同期処理が、より簡潔により強力に書けるようになります。軽量スレッドとはマイクロスレッド、ファイバーとも呼ばれるもので、「C10K問題」（クライアント1万台問題）と言われるI/O待ちによってクライアント数が多いとハードウェアの性能が生かしきれない問題の解決策の1つです。I/O待ちの際に高速にコンテキストスイッチして他のクライアントを捌くことでハードウェアの性能を限界まで引き出すプログラムを書くことが出来るようになります。つまり使いこなすとnginxのように高速軽量に動作するサーバを書くことが出来る様になります。


# ベンチマーク結果
ベンチマークの結果はPythonのasync/awaitを利用したコルーチンは起動が早く、メモリ消費量が少なく、コンテキストスイッチも早かったです。まさに軽量スレッドと言える実装になっているのではないでしょうか。Erlamgの軽量スレッドは`すごいErlang本`によると消費メモリ4KByteで起動にマイクロ秒単位で起動するみたいです。Python版はメモリ消費3.44KByte,起動に60マイクロ秒とErlangと比肩する速度が出ています。

| 項目 | 値 |
|:-----------|------------:|
|コルーチン10万個の起動と終了時間|6.081sec|
|コルーチン1個あたりの起動と終了時間|0.0608ms|
|コンテキストスイッチ100万回の実行時間|51.435sec|
|コンテキストスイッチ1回あたりの実行時間|51.435μs|
|コルーチン10万個起動時の消費メモリ|146.86MByte|
|コルーチン1個あたりの消費メモリ|1.50KByte|
|コルーチン10万個起動してコンテキストスイッチ100万回実施時の消費メモリ|336.30MByte|
|コンテキストスイッチ100万回実施時の1個あたりの消費メモリ|3.44KByte|

※消費メモリについてオーバーヘッドを考慮していないので、不正確な値になっています。ご注意ください。

## Pythonのasync/awaitが向いてる処理
ネットワークI/OやファイルI/Oを複数非同期に行う処理

# async/awaitを試す
## 普通のFizzBuzzを書く
```py:普通のFizzBuzz
def fizzbuzz(i):
    if i == 15:
        return 'FizzBuzz'
    if i % 5 == 0:
        return 'Buzz'
    if i % 3 == 0:
        return 'Fizz'
    return str(i)


for x in range(1, 10):
    print(fizzbuzz(x))

```

## async/awaitでコルーチン1個のFizzBuzz
コルーチンで実行しているだけなので結果は同じになります。
![スクリーンショット 2015-11-03 18.59.47.png](https://qiita-image-store.s3.amazonaws.com/0/65312/8615f53a-a991-5de8-5b57-b4130ef10e05.png)



```py:コルーチン1個
import asyncio
async def main():
    await task_fizzbuzz()


async def task_fizzbuzz():
    for x in range(1, 10):
        print(fizzbuzz(x))
    return None

loop = asyncio.get_event_loop()
# コルーチンでmain関数を呼び出し
loop.run_until_complete(main())
loop.close()
```

## async/awaitでコルーチン10万個のFizzBuzzを非同期実行
コルーチンを10万個生成して同時実行します。1つのコルーチンが実行終了すると、コンテキストスイッチして次のコルーチンが実行されます。
![スクリーンショット 2015-11-03 18.52.02.png](https://qiita-image-store.s3.amazonaws.com/0/65312/c481b31a-f40d-c983-fd6e-31bab3537885.png)




```py:コルーチン10万同時実行
# -*- coding: utf-8 -*-
# from asyncio import Queue
# from queue import Queue
import asyncio

async def task_fizzbuzz(prefix):
    for x in range(1, 10):
        # await asyncio.sleep(1)
        print(prefix + "{}:".format(str(x)) + fizzbuzz(x))
    return None

loop = asyncio.get_event_loop()
# コルーチン10万個生成
tasks = asyncio.wait([task_fizzbuzz(str(i) + ":") for i in range(1, 100000)])
loop.run_until_complete(tasks)
loop.close()

```

```shell-session:コルーチン10万個実行結果
....
71798:7:7
71798:8:8
71798:9:Fizz
84034:1:1
84034:2:2
84034:3:Fizz
84034:4:4
84034:5:Buzz
84034:6:Fizz
84034:7:7
84034:8:8
84034:9:Fizz
17235:1:1
17235:2:2
....

```


## async/awaitでコルーチン10万個のFizzBuzzをsleep付きで非同期実行
fizzbuzzロジック中にsleepしたとき上手くコンテキストスイッチするかのテストです。

```py:sleep付きのコルーチン10万同時実行
# -*- coding: utf-8 -*-
# from asyncio import Queue
# from queue import Queue
import asyncio

async def task_fizzbuzz(prefix):
    for x in range(1, 10):
        await asyncio.sleep(1) # 新しく追加したコード
        print(prefix + "{}:".format(str(x)) + fizzbuzz(x))
    return None

loop = asyncio.get_event_loop()
# コルーチン10万個生成
tasks = asyncio.wait([task_fizzbuzz(str(i) + ":") for i in range(1, 100000)])
# 並列実行
loop.run_until_complete(tasks)
loop.close()

```

```shell-session:sleep付きのコルーチン10万個実行結果
....
75219:6:Fizz
8282:6:Fizz
57464:6:Fizz
75220:6:Fizz
8283:6:Fizz
57465:6:Fizz
75221:6:Fizz
8284:6:Fizz
57466:6:Fizz
75222:6:Fizz
8285:6:Fizz
57467:6:Fizz
75223:6:Fizz
....

```
コルーチン実行中に`await asyncio.sleep(1)`でsleepすると他のコルーチンにスイッチするため同時実行しているような結果が得られました。
![スクリーンショット 2015-11-03 19.25.56.png](https://qiita-image-store.s3.amazonaws.com/0/65312/d74082d0-5328-ba02-d192-101f353fdc78.png)



# ベンチマークを取る
検証が終わったので次の3つのプログラムでベンチマークを取っていきます。

1. コルーチンを利用せずFizzBuzz 10回 x 10万回実行するプログラム
2. コルーチンを10万個立ち上げてFizzBuzz 10回 x 10万回実行するプログラム
3. コルーチンを10万個立ち上げてFizzBuzz1回毎にSleepを実行することでコンテキストスイッチを計100万回実行する、FizzBuzz 10回 x 10万回実行するプログラム

![スクリーンショット 2015-11-03 19.46.41.png](https://qiita-image-store.s3.amazonaws.com/0/65312/ba935ee7-c819-7132-18e4-f61bf925ee49.png)



## 1. コルーチンなし
```py
# -*- coding: utf-8 -*-
import time

def fizzbuzz(i):
    if i == 15:
        return 'FizzBuzz'
    if i % 5 == 0:
        return 'Buzz'
    if i % 3 == 0:
        return 'Fizz'
    return str(i)

COUNT = 100000
FIZZBUZZ_COUNT = 10

# 計測開始
ts = time.time()

# 10回FizzBuzz x 10万回実行
for x in range(COUNT):
    for x in range(FIZZBUZZ_COUNT):
        print(fizzbuzz(x))
# 他と条件を同じにするために10秒待つ
for x in range(FIZZBUZZ_COUNT):
    time.sleep(1)

# 計測終了
te = time.time()

# 結果出力
print("{}sec".format(te-ts))


```

## 2.コルーチン10万個立ち上げ
```py
# -*- coding: utf-8 -*-
import time
import asyncio

def fizzbuzz(i):
    if i == 15:
        return 'FizzBuzz'
    if i % 5 == 0:
        return 'Buzz'
    if i % 3 == 0:
        return 'Fizz'
    return str(i)

COUNT = 100000
FIZZBUZZ_COUNT = 10

# 計測開始
ts = time.time()

# 10回FizzBuzz x 10万回実行
async def task_fizzbuzz(prefix):
    for x in range(FIZZBUZZ_COUNT):
        # await asyncio.sleep(1)
        print(prefix + "{}:".format(str(x)) + fizzbuzz(x))
    return None

loop = asyncio.get_event_loop()
tasks = asyncio.wait([task_fizzbuzz(str(i) + ":") for i in range(COUNT)])
loop.run_until_complete(tasks)
loop.close()

# 他と条件を同じにするために10秒待つ
for x in range(FIZZBUZZ_COUNT):
    time.sleep(1)

# 計測終了
te = time.time()

# 結果出力
print("{}sec".format(te-ts))

```


## 3.コルーチン10万個立ち上げコンテキストスイッチ100万回
```py
# -*- coding: utf-8 -*-
import time
import asyncio

def fizzbuzz(i):
    if i == 15:
        return 'FizzBuzz'
    if i % 5 == 0:
        return 'Buzz'
    if i % 3 == 0:
        return 'Fizz'
    return str(i)

COUNT = 100000
FIZZBUZZ_COUNT = 10

# 計測開始
ts = time.time()

# 10回FizzBuzz x 10万回実行
async def task_fizzbuzz(prefix):
    for x in range(FIZZBUZZ_COUNT):
        await asyncio.sleep(1)
        print(prefix + "{}:".format(str(x)) + fizzbuzz(x))
    return None

loop = asyncio.get_event_loop()
tasks = asyncio.wait([task_fizzbuzz(str(i) + ":") for i in range(COUNT)])
loop.run_until_complete(tasks)
loop.close()

# 計測終了
te = time.time()

# 結果出力
print("{}sec".format(te-ts))

```

# ベンチマーク結果
各3回しかやってないよ。コンテキストスイッチ時間掛かりすぎぃ(›´ω`‹ )　
結果を単純にするために書いてないけどベンチマーク2もコンテキストスイッチ10万回発生してそう。

![スクリーンショット 2015-11-03 19.58.15.png](https://qiita-image-store.s3.amazonaws.com/0/65312/ab5bbb36-efec-8476-8196-71f19ca7199d.png)

![スクリーンショット 2015-11-03 19.50.43.png](https://qiita-image-store.s3.amazonaws.com/0/65312/530d547f-2377-c24a-7690-a26bdded0e46.png)



# 参考
[PEP 0492 -- Coroutines with async and await syntax](https://www.python.org/dev/peps/pep-0492/)
[18.5.3. タスクとコルーチン](http://docs.python.jp/3/library/asyncio-task.html#asyncio.async)
[18.5.9. Develop with asyncio](https://docs.python.org/3/library/asyncio-dev.html)
[Python3.5の新機能](http://qiita.com/ksato9700/items/9a0aef07d90d00e67525)
[How to use async/await in Python 3.5?](http://stackoverflow.com/questions/32808893/how-to-use-async-await-in-python-3-5)
[Async および Await を使用した非同期プログラミング (C# および Visual Basic)](https://msdn.microsoft.com/ja-jp/library/hh191443.aspx)
[AsyncIO vs Gevent? : Python - Reddit](https://www.reddit.com/r/Python/comments/1s7eae/asyncio_vs_gevent/)）