1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

asyncio で JS の非同期と同じことをする

Last updated at Posted at 2024-06-28

Python の asyncio が分からないので、とりあえず JavaScript で簡単な非同期プログラムを作って、それと同じことを asyncio でやってみようと思いました。

前提知識

JavaScript の非同期処理 (async, await, Promise) は分かっている前提です。
まだ良く分かっていない方にはこちらの記事をお勧めします。ただしコールバックやアロー関数を理解していることが前提になります。

【図解】1から学ぶ JavaScript の 非同期処理

Python については、関数の書き方が分かっていれば大丈夫です。

概要

以下の順番でプログラムを作ります。
各実装の JS 版と Python 版を見比べたい場合は 1->4, 2->5, 3->6 の順に読んでください。

  1. JS で非同期関数を逐次実行する
  2. JS で非同期関数を並行実行する
  3. JS で非同期関数をまとめて並行実行する
  4. asyncio で非同期関数 (コルーチン) を逐次実行する
  5. asyncio で非同期関数 (コルーチン) を並行実行する
  6. asyncio で非同期関数 (コルーチン) をまとめて並行実行する

JS 版

node xx.js 等として実行できるように Node.js をインストールしておきます。

環境

Windows: 10
node: v20.11.1

以下、async-js というディレクトリでプログラムを書いたり実行したりします。

共通で使う非同期関数

まず、共通で使う非同期関数を作ります。
非同期処理の解説でよくある「何秒待つ」みたいなのはやめて、処理が完了した瞬間を実感できるようにしました。

下記の findFile() は、指定のディレクトリに指定のファイルが見つかるまで待つ、という非同期関数です。
この関数については説明しませんが、この通り書けば動きます。

async-js/find-file.js
const fs = require('fs');
const path = require('path');

// 指定した時間が経過したら resolve する非同期関数
function delay(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

// ディレクトリにファイルが作られるまで待って、true を返す
async function findFile(directory, filename) {
  const filePath = path.join(directory, filename);

  while (true) {
    if (fs.existsSync(filePath)) {
      return true;
    } else {
      console.log(`${filename} を待機中...`);
      await delay(1000)
    }
  }
}

module.exports = { findFile };

ではこの関数を呼び出すコードを書いてみます。
同じディレクトリに以下の wait-file.js と、新規で空の done フォルダを作ります。

async-js/wait-file.js
const path = require('path');
const { findFile } = require('./find-file');

const doneDir = path.join(__dirname, 'done');

// ファイル「ごはん.txt」が見つかるまで待つ非同期関数を呼ぶ
findFile(doneDir, 'ごはん.txt');

wait-file.js を実行すると以下のように「ごはん.txt を待機中...」というメッセージが1秒ごとに表示されます。

async-js> node wait-file.js
ごはん.txt を待機中...
ごはん.txt を待機中...
ごはん.txt を待機中...
...

ここで、done フォルダに「ごはん.txt」というファイルを入れると...

図01-doneフォルダ.png

処理が完了してプロンプトに戻ります。

...
ごはん.txt を待機中...
ごはん.txt を待機中...
ごはん.txt を待機中...
async-js> _

ディレクトリにファイルを入れれば処理が完了するので、完了した瞬間が分かりやすいと思います。

1. findFile() を逐次実行する

では最初のプログラムです。
JavaScript で、先ほどの非同期関数 findFile() を逐次実行してみます。

待機するファイルを「ごはん.txt」「おかず.txt」「汁.txt」の3つにして、findFile() を3回呼び出します。
それ以外に以下の変更を加えています。

  1. すべてのファイルが見つかったらその旨を表示するよう、見つかったファイルをチェックする関数 checkDone() を追加しました。
  2. findFile() を呼び出す前後にメッセージを表示し、全体として食事の準備をするタスクを実行している感じにしました。
async-js/cook-sync.js
const path = require('path');
const { findFile } = require('./find-file');

const doneDir = path.join(__dirname, 'done');

const tasksDone = {
  'ごはん': false,
  'おかず': false,
  '': false,
};

// task で指定したファイルが見つかった場合のチェック処理
function checkDone(task) {
  tasksDone[task] = true;

  if (tasksDone['ごはん'] && tasksDone['おかず'] && tasksDone['']) {
    console.log('\すべてのタスクが完了しました。/');
  }
}

// メインの処理で await するために、無名の非同期関数の中でやる
(async () => {
  console.log('「ごはん」を炊いてください。');
  // ファイルができるまで待つ非同期関数を呼んで、完了するまで待つ
  await findFile(doneDir, 'ごはん.txt');
  console.log('** ごはんが炊けました。**');
  // すべてのタスクが完了したかチェックする処理を呼ぶ
  checkDone('ごはん');

  console.log('「おかず」を作ってください。');
  // ファイルができるまで待つ非同期関数を呼んで、完了するまで待つ
  await findFile(doneDir, 'おかず.txt');
  console.log('** おかずができました。**');
  // すべてのタスクが完了したかチェックする処理を呼ぶ
  checkDone('おかず');

  console.log('「汁」を温めてください。');
  // ファイルができるまで待つ非同期関数を呼んで、完了するまで待つ
  await findFile(doneDir, '汁.txt');
  console.log('** 汁が温まりました。**');
  // すべてのタスクが完了したかチェックする処理を呼ぶ
  checkDone('');
})();

done フォルダを空にして実行すると、まず「ごはん.txt」を待機します。

async-js> node cook-sync.js
「ごはん」を炊いてください。
ごはん.txt を待機中...
ごはん.txt を待機中...
...

done フォルダに「ごはん.txt」から順番に入れていきます。

図02-doneフォルダ-02.png

「ごはん.txt」を入れると、引き続き「おかず.txt」を待機します。

ごはん.txt を待機中...
** ごはんが炊けました。**
「おかず」を作ってください。
おかず.txt を待機中...      
おかず.txt を待機中...
...

done フォルダに「おかず.txt」を入れると、引き続き「汁.txt」を待機します。

おかず.txt を待機中...
** おかずができました。**
「汁」を温めてください。
汁.txt を待機中...
汁.txt を待機中...
...

done フォルダに「汁.txt」を入れると、すべて完了したメッセージを表示してプロンプトに戻ります。

汁.txt を待機中...
** 汁が温まりました。**
\すべてのタスクが完了しました。/
async-js> _

2. findFile() を並行実行する

次に、findFile() を並行実行してみます。
コードは以下のようになります。

async-js/cook-async.js
// 前略
// checkDone() 関数の定義までは さっきと同じ

console.log('「ごはん」を炊いてください。');
// ファイルができるまで待つ非同期関数を呼んで、完了したら
// すべてのタスクが完了したかチェックする処理を呼ぶ
findFile(doneDir, 'ごはん.txt').then((result) => {
  console.log('** ごはんが炊けました。**');
  checkDone('ごはん');
});

console.log('「おかず」を作ってください。');
// ファイルができるまで待つ非同期関数を呼んで、完了したら
// すべてのタスクが完了したかチェックする処理を呼ぶ
findFile(doneDir, 'おかず.txt').then((result) => {
  console.log('** おかずができました。**');
  checkDone('おかず');
});

console.log('「汁」を温めてください。');
// ファイルができるまで待つ非同期関数を呼んで、完了したら
// すべてのタスクが完了したかチェックする処理を呼ぶ
findFile(doneDir, '汁.txt').then((result) => {
  console.log('** 汁が温まりました。**');
  checkDone('');
});

done フォルダを空にして実行すると、「ごはん.txt」「おかず.txt」「汁.txt」の3つを同時に待機し始めます

async-js> node cook-async.js
「ごはん」を炊いてください。
ごはん.txt を待機中...
「おかず」を作ってください。
おかず.txt を待機中...
「汁」を温めてください。
汁.txt を待機中...
ごはん.txt を待機中...
おかず.txt を待機中...
汁.txt を待機中...
...

先に「汁.txt」を done フォルダに入れると、「汁.txt」を待機する処理だけが完了します。

おかず.txt を待機中...
** 汁が温まりました。**
ごはん.txt を待機中...
おかず.txt を待機中...
ごはん.txt を待機中...
おかず.txt を待機中...

次に「おかず.txt」を done フォルダに入れてみます。

おかず.txt を待機中...
ごはん.txt を待機中...
** おかずができました。**
ごはん.txt を待機中...
ごはん.txt を待機中...
...

最後に「ごはん.txt」を done フォルダに入れると すべての処理が完了します。

ごはん.txt を待機中...
** ごはんが炊けました。**
\すべてのタスクが完了しました。/
async-js> _

つまり、順番に関係なく 待機しているファイルのいずれかを done フォルダに入れれば それを待機していた処理が完了 します。
そしてそのタイミングで、「完了した時に実行する処理」が呼ばれます。
これが並行実行です。

3. findFile() をまとめて並行実行する

最後に、findFile() をまとめて実行する時の書き方です。
つまり、「ごはん.txt」「おかず.txt」「汁.txt」の待機状態をそれぞれで開始するのでなく まとめて開始 します。
そして完了時の処理もそれぞれで呼ぶのでなく、すべてが完了した時に一度だけ 呼ぶようにします。

async-js/cook-async-all.js
const path = require('path');
const { findFile } = require('./find-file');

const doneDir = path.join(__dirname, 'done');

console.log('「ごはん」を炊いてください。');
promise1 = findFile(doneDir, 'ごはん.txt');

console.log('「おかず」を作ってください。');
promise2 = findFile(doneDir, 'おかず.txt');

console.log('「汁」を温めてください。');
promise3 = findFile(doneDir, '汁.txt');

Promise.all([promise1, promise2, promise3]).then((values) => {
  console.log('\すべてのタスクが完了しました。/');
});

done フォルダを空にして実行し、先ほどと同じように「汁.txt」>「おかず.txt」>「ごはん.txt」の順に done フォルダに入れると、以下のようになります。

async-js> node cook-async-all.js
「ごはん」を炊いてください。
ごはん.txt を待機中...
「おかず」を作ってください。
おかず.txt を待機中...
「汁」を温めてください。
汁.txt を待機中...
ごはん.txt を待機中...
おかず.txt を待機中...
汁.txt を待機中...
ごはん.txt を待機中...
おかず.txt を待機中...
汁.txt を待機中...        (ここで「汁.txt」を入れる)
ごはん.txt を待機中...
おかず.txt を待機中...
ごはん.txt を待機中...
おかず.txt を待機中...    (ここで「おかず.txt」を入れる)
ごはん.txt を待機中...
ごはん.txt を待機中...
ごはん.txt を待機中...    (ここで「ごはん.txt」を入れる)
\すべてのタスクが完了しました。/
async-js> _

Python 版 (asyncio)

1~3 でやったことを、asyncio でやってみます。

環境

Python 3.12.3

以下、async-py というディレクトリでプログラムを書いたり実行したりします。

共通で使う非同期関数

JS 版で findFile() として作った関数を、find_file() という名前で作ります。
JS 版では動作確認のためのシンプルなプログラム (wait-file.js) を作りましたが、Python 版ではこのモジュールを直に実行できるようにしました (最後の if 文のところ)。

async-py/find_file.py
import asyncio
from pathlib import Path

async def find_file(directory, filename):
    '''ディレクトリにファイルが作られるまで待って、true を返す
    '''
    file_path = directory / filename

    while True:
        if file_path.exists():
            return True
        else:
            print(f'{filename} を待機中...')
            await asyncio.sleep(1)

if __name__ == '__main__':
    done_dir = Path(__file__).resolve().parent / 'done'
    asyncio.run(find_file(done_dir, 'ごはん.txt'))

find_file() のように async def で宣言された関数のことを「コルーチン関数」或は単に「コルーチン」と言います。

JS 版と同様に done フォルダを作って このモジュールを直に実行すると、「ごはん.txt」を待機します。

async-py> python find_file.py
ごはん.txt を待機中...
ごはん.txt を待機中...
ごはん.txt を待機中...
...

done フォルダに「ごはん.txt」を入れると処理が完了します。

...
ごはん.txt を待機中...
ごはん.txt を待機中...
ごはん.txt を待機中...
async-py> _

4. asyncio で find_file() を逐次実行する

Python の asyncio を使った書き方は以下のようになります。
await はコルーチンの中でしか使えないので、代わりに asyncio.run() を使います。

async-py/cook-sync.py
import asyncio
from pathlib import Path
from find_file import find_file

done_dir = Path(__file__).resolve().parent / 'done'

tasks_done = {
    'ごはん': False,
    'おかず': False,
    '': False,
}

def check_done(task):
    '''task で指定したファイルが見つかった場合のチェック処理
    '''
    tasks_done[task] = True

    if tasks_done['ごはん'] and tasks_done['おかず'] and tasks_done['']:
        print('\すべてのタスクが完了しました。/')

print('「ごはん」を炊いてください。')
# ファイルができるまで待つ非同期関数を呼んで、完了するまで待つ
asyncio.run(find_file(done_dir, 'ごはん.txt'))
print('** ごはんが炊けました。**')
# すべてのタスクが完了したかチェックする処理を呼ぶ
check_done('ごはん')

print('「おかず」を作ってください。')
# ファイルができるまで待つ非同期関数を呼んで、完了するまで待つ
asyncio.run(find_file(done_dir, 'おかず.txt'))
print('** おかずができました。**')
# すべてのタスクが完了したかチェックする処理を呼ぶ
check_done('おかず')

print('「汁」を温めてください。')
# ファイルができるまで待つ非同期関数を呼んで、完了するまで待つ
asyncio.run(find_file(done_dir, '汁.txt'))
print('** 汁が温まりました。**')
# すべてのタスクが完了したかチェックする処理を呼ぶ
check_done('')

done フォルダを空にして実行すると、JS 版と同じであることが確認できます。

async-py> python cook_sync.py
「ごはん」を炊いてください。
ごはん.txt を待機中...
ごはん.txt を待機中...      (ここで「ごはん.txt」を入れる)
** ごはんが炊けました。**
「おかず」を作ってください。
おかず.txt を待機中...
おかず.txt を待機中...      (ここで「おかず.txt」を入れる)
** おかずができました。**
「汁」を温めてください。
汁.txt を待機中...
汁.txt を待機中...          (ここで「汁.txt」を入れる)
** 汁が温まりました。**
\すべてのタスクが完了しました。/
async-py> _

5. asyncio で find_file() を並行実行する

asyncio での並行実行は以下のようになります。
find_file() が完了したら ○○する」というのを JS みたいにコールバックでやるのでなく、コルーチンを作ってその中でやる 必要があるようです。

async-py/cook_async.py
# 前略
# check_done() 関数の定義までは さっきと同じ

async def ごはんを炊く():
    '''ファイルができるまで待つ非同期関数を呼んで、完了したら
    すべてのタスクが完了したかチェックする処理を呼ぶコルーチン
    '''
    print('「ごはん」を炊いてください。')
    await find_file(done_dir, 'ごはん.txt')
    print('** ごはんが炊けました。**')
    check_done('ごはん')

async def おかずを作る():
    '''ファイルができるまで待つ非同期関数を呼んで、完了したら
    すべてのタスクが完了したかチェックする処理を呼ぶコルーチン
    '''
    print('「おかず」を作ってください。')
    await find_file(done_dir, 'おかず.txt')
    print('** おかずができました。**')
    check_done('おかず')

async def 汁を温める():
    '''ファイルができるまで待つ非同期関数を呼んで、完了したら
    すべてのタスクが完了したかチェックする処理を呼ぶコルーチン
    '''
    print('「汁」を温めてください。')
    await find_file(done_dir, '汁.txt')
    print('** 汁が温まりました。**')
    check_done('')

# メインの処理で await するために、コルーチンの中でやる
async def main():
    # コルーチンを Task 化する
    ごはん_task = asyncio.create_task(ごはんを炊く())
    おかず_task = asyncio.create_task(おかずを作る())
    汁_task = asyncio.create_task(汁を温める())

    # Task を並行実行する (Task を await した場合は完了を待たない)
    await ごはん_task
    await おかず_task
    await 汁_task

asyncio.run(main())

コルーチンを非同期実行するには、create_task()Task オブジェクト を作る必要があるようです。
done フォルダを空にして、実行してみます。

async-py> python cook_async.py
「ごはん」を炊いてください。
ごはん.txt を待機中...
「おかず」を作ってください。
おかず.txt を待機中...
「汁」を温めてください。
汁.txt を待機中...
ごはん.txt を待機中...
おかず.txt を待機中...
汁.txt を待機中...
ごはん.txt を待機中...      (ここで「おかず.txt」を入れる)
** おかずができました。**
汁.txt を待機中...
ごはん.txt を待機中...
汁.txt を待機中...
ごはん.txt を待機中...      (ここで「汁.txt」を入れる)
** 汁が温まりました。**
ごはん.txt を待機中...
ごはん.txt を待機中...      (ここで「ごはん.txt」を入れる)
** ごはんが炊けました。**
\すべてのタスクが完了しました。/
async-py> _

これも JS 版と同じ挙動になることが確認できました。
Task を実行する時は await が必要で、await といっても Task の場合は完了を待つ訳ではない ことに注意が必要です。

6. asyncio で find_file() をまとめて並行実行する

JS の Promise.all() と同じようなことをするのに、asyncio.gather() を使います。
ただし、これも await をつけて実行して、すべて完了した時の処理はその下に書きます。

async-py/cook_async_all.py
import asyncio
from pathlib import Path

from find_file import find_file

done_dir = Path(__file__).resolve().parent / 'done'

# メインの処理で await するために、コルーチンの中でやる
async def main():
    print('「ごはん」を炊いてください。')
    ごはん_task = asyncio.create_task(find_file(done_dir, 'ごはん.txt'))

    print('「おかず」を作ってください。')
    おかず_task = asyncio.create_task(find_file(done_dir, 'おかず.txt'))

    print('「汁」を温めてください。')
    汁_task = asyncio.create_task(find_file(done_dir, '汁.txt'))

    await asyncio.gather(ごはん_task, おかず_task, 汁_task)
    print('\すべてのタスクが完了しました。/')

asyncio.run(main())

done フォルダを空にして実行すると以下のようになります。

async-py> python cook_async_all.py
「ごはん」を炊いてください。
「おかず」を作ってください。
「汁」を温めてください。
ごはん.txt を待機中...
おかず.txt を待機中...
汁.txt を待機中...
ごはん.txt を待機中...
おかず.txt を待機中...  (ここで「おかず.txt」を入れる)
汁.txt を待機中...
ごはん.txt を待機中...
汁.txt を待機中...
ごはん.txt を待機中...  (ここで「ごはん.txt」を入れる)
汁.txt を待機中...
汁.txt を待機中...
汁.txt を待機中...      (ここで「汁.txt」を入れる)
\すべてのタスクが完了しました。/
async-py> _

JS 版と比べると「「ごはん」を炊いてください。」等のメッセージが表示されるタイミングが違いましたが、並行処理は同じように行われました。
同じことをするのに、TaskGroup を使う方法 もあるようです。

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?