はじめに
@y_kato_eng (Yuuki Kato) さんの記事「非同期処理をシンプルなPythonコードで説明する」の冒頭に文章で書かれている処理をPythonで書いてみようと思いました。しかし、非同期処理を意図したものが同期処理になってしまい、シンプルに書けなかったので紹介いたします。
作成したい非同期処理
10分かけてご飯を炊いている間に、2分かけて食材を切り、5分かけて部屋を掃除するサンプルコードを書こうと思いました。
そこで、asyncio.sleepを使用して、10秒間スリープするタスクと、2秒スリープした後に5秒スリープするタスクの2つを非同期で実行し、全体で10秒で終わるサンプルコードを作成してみることにしました。実際に10分かけて動作させると待つのが大変なので10秒に短縮しています。
JavaScriptでのサンプルコード
JavaScriptで非同期処理を書いた経験があるので、まずはJavaScriptでのサンプルコードを示します。
JavaScriptではsetTimeout関数とPromiseを組み合わせて非同期sleep関数を作ります。
以下のコードを実行すると全処理が10秒で終わります。
start = Date.now()
function sleep(seconds) {
return new Promise(resolve => setTimeout(resolve, seconds * 1000))
}
function log(message) {
console.log(`${Math.trunc((Date.now() - start) / 1000)}秒経過`, message)
}
async function ご飯を炊く() {
log("ご飯を炊きます")
await sleep(10)
log("10分かかってご飯が炊けました")
}
async function 食材を切る() {
log("食材を切ります")
await sleep(2)
log("2分かけて食材を切りました")
}
async function 部屋の掃除をする() {
log("部屋を掃除します")
await sleep(5)
log("5分かけて部屋を掃除しました")
}
async function main() {
log("タスク開始")
const 裏タスク = ご飯を炊く()
await 食材を切る()
await 部屋の掃除をする()
await 裏タスク
log("タスク終了")
}
main()
0秒経過 タスク開始
0秒経過 ご飯を炊きます
0秒経過 食材を切ります
2秒経過 2分かけて食材を切りました
2秒経過 部屋を掃除します
7秒経過 5分かけて部屋を掃除しました
10秒経過 10分かかってご飯が炊けました
10秒経過 タスク終了
Pythonに単純移植したサンプルコード
Pythonにそのまま移植すると、全処理に17秒かかってしまいました。
import asyncio
import datetime
start = datetime.datetime.now()
def log(message):
print(f"{(datetime.datetime.now() - start).seconds}秒経過", message)
async def ご飯を炊く():
log("ご飯を炊きます")
await asyncio.sleep(10)
log("10分かかってご飯が炊けました")
async def 食材を切る():
log("食材を切ります")
await asyncio.sleep(2)
log("2分かけて食材を切りました")
async def 部屋の掃除をする():
log("部屋を掃除します")
await asyncio.sleep(5)
log("5分かけて部屋を掃除しました")
async def main():
log("タスク開始")
裏タスク = ご飯を炊く()
await 食材を切る()
await 部屋の掃除をする()
await 裏タスク
log("タスク終了")
asyncio.run(main())
0秒経過 タスク開始
0秒経過 食材を切ります
2秒経過 2分かけて食材を切りました
2秒経過 部屋を掃除します
7秒経過 5分かけて部屋を掃除しました
7秒経過 ご飯を炊きます
17秒経過 10分かかってご飯が炊けました
17秒経過 タスク終了
全処理が10秒で終了するように対処したサンプルコード
pythonでasyncio.sleepを同時に動かすには、asyncio.gatherを使用する必要がありました。
タスクごとに関数を作り、asyncio.gatherの引数に渡します。
import asyncio
import datetime
start = datetime.datetime.now()
def log(message):
print(f'{(datetime.datetime.now() - start).seconds}秒経過', message)
async def ご飯を炊く():
log("ご飯を炊きます")
await asyncio.sleep(10)
log("10分かかってご飯が炊けました")
async def 食材を切る():
log("食材を切ります")
await asyncio.sleep(2)
log("2分かけて食材を切りました")
async def 部屋の掃除をする():
log("部屋を掃除します")
await asyncio.sleep(5)
log("5分かけて部屋を掃除しました")
async def 裏タスク():
await ご飯を炊く()
async def 表タスク():
await 食材を切る()
await 部屋の掃除をする()
async def main():
log("タスク開始")
await asyncio.gather(裏タスク(), 表タスク())
log("タスク終了")
asyncio.run(main())
0秒経過 タスク開始
0秒経過 ご飯を炊きます
0秒経過 食材を切ります
2秒経過 2分かけて食材を切りました
2秒経過 部屋を掃除します
7秒経過 5分かけて部屋を掃除しました
10秒経過 10分かかってご飯が炊けました
10秒経過 タスク終了
全処理が10秒で終了するように対処したサンプルコード(その2)
@tenmyo さんから、create_taskを教えていただきました。
シンプルにはなりましたが、メッセージ出力順序がちょっと違っています。
import asyncio
import datetime
start = datetime.datetime.now()
def log(message):
print(f"{(datetime.datetime.now() - start).seconds}秒経過", message)
async def ご飯を炊く():
log("ご飯を炊きます")
await asyncio.sleep(10)
log("10分かかってご飯が炊けました")
async def 食材を切る():
log("食材を切ります")
await asyncio.sleep(2)
log("2分かけて食材を切りました")
async def 部屋の掃除をする():
log("部屋を掃除します")
await asyncio.sleep(5)
log("5分かけて部屋を掃除しました")
async def main():
log("タスク開始")
裏タスク = asyncio.create_task(ご飯を炊く())
await 食材を切る()
await 部屋の掃除をする()
await 裏タスク
log("タスク終了")
asyncio.run(main())
0秒経過 タスク開始
0秒経過 食材を切ります
0秒経過 ご飯を炊きます
2秒経過 2分かけて食材を切りました
2秒経過 部屋を掃除します
7秒経過 5分かけて部屋を掃除しました
10秒経過 10分かかってご飯が炊けました
10秒経過 タスク終了
さいごに
JavaScriptではタイマースレッドが裏で動いており、setTimerはタイマースレッドに処理依頼するため、シンプルな記述で同時実行できます。
一方、Pythonにはタイマースレッドがないため、async.gatherを使用して複数のasync.sleep処理を同時に実行させる必要がありました。
そのため、タスクごとに処理をまとめた関数を作成する必要があり、シンプルに書けない例になりました。