はじめに
@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処理を同時に実行させる必要がありました。
そのため、タスクごとに処理をまとめた関数を作成する必要があり、シンプルに書けない例になりました。