Python

【Python】コルーチンやasyncioライブラリを使って非同期処理を書いてみよう

コルーチンやasyncioライブラリを使って非同期処理を書いてみようPython

前回の記事でPythonの外部パッケージの説明として、画像編集のための外部モジュール「pillow」について使い方などを説明しました。

前回までは主に文法からライブラリの使い方まで一通りのことを学んできました。

今回は、少し応用的な内容になりますが「非同期処理」について説明したいと思います。

この記事を読んで分かること
  • Pythonでの非同期処理についての内容がわかる
  • asyncioパッケージについてその内容がわかる
  • コルーチンやasync、awaitの使い方についてわかる
スポンサーリンク

非同期処理とは

非同期処理ってなんでしょうか?

そうですね、そこからですよね

今まで作成してきたプログラムは「処理が終わったら次の処理をする」というように処理が順番に進んでいくものでした。

条件分岐や繰り返しなども処理を飛ばしたり繰り返したりしても、基本的には処理が終わってから新しい処理に移行します。

また関数やクラスなどを作成した場合でも、呼び出した関数やメソッドを順に実行していく点は変わりません。

例えば、2つの関数を続けて呼び出しても、必ず最初の関数処理が先に実施されて、それが完了してから次の関数の処理が実行されます

ですので、プログラム上の処理は常に「1つずつ順番に実行」が基本でした。

ですが、プログラムにおいて「ある処理と、別の処理を同時に実行する」ということが必要になる場合があります。

例えば、Webブラウザでダウンロードしながら他のページを表示したりとかですね

そうそう、そんな感じです。

そういった処理に時間がかかる関数がある時に、その関数処理が終わるまで次の実行をずっと待たなければいけないなると柔軟なプログラムが作成できなくなります。

ですので、これまでの1つの処理で進む方法に対して、複数の処理を並行して実施する方法もプログラム作成において必要なことです。

このように「常に1つの処理が完了したら次の処理に進む」という処理を「同期処理」と呼びます。

これに対し「処理Aと処理Bを並列して実行する」という処理を「非同期処理」と呼びます。

  • 同期処理:常に一つの処理で進む
  • 非同期処理:複数の処理が並行で進む
ポイント

同期処理は時間のかかる処理も完了するまで待って次に進むが、非同期処理だと時間のかかる処理と並行して別の処理を実行できる

スポンサーリンク

asyncとコルーチン

Pythonでプログラムを作成する場合にも、こうした非同期処理を利用できます

これを実現するには「async」と「await」という構文を使います。

「async」は、コルーチンを定義します。

async def 関数名( 引数 ) :
    …関数内で実施する処理…

コルーチンってなんですか?

コルーチンとは「処理の実行を一時停止したり再開したりできる特殊な関数」です。

基本的には通常の関数と同じように定義されますが、必要に応じて処理を途中で停止したり再開したりできます。

このコルーチンの中で「await」という構文を使います。

await コルーチン

awaitは、実行中のコルーチンを一時停止して別のコルーチンに処理を渡します

いくつかのコルーチンを実行し、その中でawaitして処理の実行を他のコルーチンに切り替えられるようにすることで、それぞれのコルーチンの処理が少しずつ進められるようなプログラムが作成できます。

ポイント

非同期処理は、asyncを定義しコルーチンとして作成する。コルーチンでは、awaitで処理を切り替えられるようにする。

コルーチンの非同期は「切り替えながら動く」

ここで1つ疑問が発生します。

それって並行処理じゃなくない、複数の処理をちょっとづつ切り替えながら実施してるなら結局1つの処理じゃないの

はい、その通りです。

この非同期処理は、実は「同時に複数の処理を並行して実行するための機能」ではなくて、「複数の処理を切り替えながら並行して少しずつ実行できる機能」となります。

つまりこのコルーチンを使った非同期処理は「高速で複数の処理を切り替えながら実行していくことで、同時に複数の処理を進めているのと同等の処理を実現するもの」と考えてもらえると良いかと思います。

スポンサーリンク

コルーチンとイベントループ

コルーチンを利用する場合、イベントループと呼ばれるループ処理の中で実行しなければなりません。

イベントループとは、常に様々な入力を待ち受けながらエンドレスで繰り返しているループ処理です。

プログラムが必要に応じてこのイベントループに処理を追加するとイベントループは追加された処理を順に実行していきます。

このイベントループの中でコルーチンを実行することで、必要に応じてコルーチンを一時停止したり再開したりしながら処理を進めていけるようになります。

なのでイベントループ内でなければ、コルーチンの一時停止などの要求を受け取って処理することができませんので注意しましょう

ポイント

コルーチンは必ず「イベントループ」の中で実行すること

コルーチンの関数

とまあ、イベントループの説明をしましたが、結論から言うとイベントループは意識する必要はありません

というのも以前のPythonでは、プログラムでイベントループを取得して、イベントループ内にコルーチンを設定して、処理して、イベントループを閉じてみたいな処理を書く必要がありましたが、今はPythonのバージョンも上がりイベントループを意識せずに非同期が行えるようになりました

Pythonは日々進化しているのさ

ですので、コルーチンを使った非同期の処理をする場合は、基本的に下記2つの関数を覚えれば良いです。

まずは、コルーチンはasyncioパッケージに用意されている関数を利用しますので、このasyncioパッケージをインポートします。

from asyncio import *

コルーチンの実行には「run」関数を使います。

これは引数に指定したコルーチンをrun関数内で新たに作成したイベントループで実行します。
また、コルーチンの処理が終了すると自動的にイベントループを終了します。

run(コルーチン)

コルーチンを複数まとめて並行処理をする場合は「gather」関数を使います。

ただしrun関数の引数がコルーチンしか受け取れませんので、gatherでまとめたものを別コルーチンで定義してrun関数の引数に渡します。

gather(コルーチン1,コルーチン2,…..)

この辺の使い方は若干わかりにくいので、詳細はプログラムで説明します

ポイント

コルーチンの実行は、run関数の引数にコルーチンを指定して実行する。コルーチンが複数ある場合は、gather関数を使ってコルーチンをまとめて渡す。

スポンサーリンク

コルーチンを実行する

では実際に操作してみましょう。

と、その前にいきなり複数での非同期処理はわかりにくいですので、まずは単体で「イベントループの中でコルーチンを動かす」処理を書いてみますね。

下記は、first_fn()というコルーチンをイベントループ内で実行しています。

このfirst_fn()コルーチン内で、awaitからfn()コルーチンをさらに呼び出しており、1秒間待ってからテキスト出力します。

from asyncio import *  # asyncioのインポート

async def fn(n):
    await sleep(1)
    print("fn: num="+str(n))

async def first_fn():
    for n in range(5):
        await fn(n)

run(first_fn())

9行目のコルーチンにてawaitで別のコルーチンfnを呼び出しています。
4行目のコルーチンで1秒待ってから5行目で出力します。

ここで、4行目のsleepで処理を一時的に停止することで、その間に他のタスクが実行されるのを許可します。(今回は他のタスクがないので単独で動いています)

実行すると下記の出力が1秒間隔で出力されます。

fn: num=0
fn: num=1
fn: num=2
fn: num=3
fn: num=4

繰り返しになりますが、これは非同期処理を単体で動かした場合を説明しています、とりあえず単体の処理の動きを確認してから複数の処理を見ていきましょう

スポンサーリンク

非同期処理を実行する

では、今度は複数の非同期処理を実行してみましょう。

先ほどのプログラムを修正して、複数のコルーチンを並行して実行するように変更してみます。

from asyncio import *  # asyncioのインポート

async def fn(id,n,dy):
    await sleep(dy)
    print(id+": num="+str(n))

async def first_fn(name,dy):
    for n in range(5):
        await fn(name,n,dy)
    print("***"+name+" is finished.***")

async def main():
    await gather(first_fn("1st",0.1),
                 first_fn("2nd",0.2),
                 first_fn("3rd",0.3))

run(main())

先ほど作成した、first_fnを3つ並行して実行しています。

first_fn関数を各コルーチンの名前を設定できるようにしたのと、引数で渡したdyの値だけ一時停止するように修正しました。
よって、それぞれ「1st→0.1」「2nd→0.2」「3rd→0.3」秒ずつ停止するようにコルーチンを3つ作成して非同期で実行しました。

実行結果は以下となります。

1st: num=0
2nd: num=0
1st: num=1
3rd: num=0
1st: num=2
2nd: num=1
1st: num=3
1st: num=4
***1st is finished.***
3rd: num=1
2nd: num=2
2nd: num=3
3rd: num=2
2nd: num=4
***2nd is finished.***
3rd: num=3
3rd: num=4
***3rd is finished.***

実行結果を見ると、3つのコルーチンは決して同じようには出力されていないのと、順序も統一されていません

また、処理の停止時間をバラバラにしたため、それぞれの処理を進めていくスピードにも差があることがわかります。

この出力を見ると、sleepに0.1が設定された1stが一番処理が速く、0.3が設定された3rdが最も遅くなっていることがわかります。

また、13行目で複数のコルーチンをgather関数でまとめていますが、run関数にはコルーチンを引数に渡す必要があるので、「main」という別コルーチンを定義してrun関数に渡しています。

sleepしないとどうなるか?

コルーチンが並行して動いたのは、4行目の「await」のsleepがあるために、コルーチン間で実行する処理が移動できました。

ではこの4行目「await sleep(dy)」の処理を削除(もしくはコメントアウト)してみるとどうなるでしょうか。

4行目を削除して実行した結果は以下となります。

1st: num=0
1st: num=1
1st: num=2
1st: num=3
1st: num=4
***1st is finished.***
2nd: num=0
2nd: num=1
2nd: num=2
2nd: num=3
2nd: num=4
***2nd is finished.***
3rd: num=0
3rd: num=1
3rd: num=2
3rd: num=3
3rd: num=4
***3rd is finished.***

3つのコルーチンが順番に実行されているだけに変わりましたね

1stが実行され、全てが終わったら次に2nd、そして2ndが全て終わったら3rdというように順番に実行されており、3つ並行して非同期に処理が実行されていません

awaitによる一時停止と実行するコルーチンの切り替えにより、3つのコルーチンがそれぞれ並行しているかのように処理が進められるようになります。

スポンサーリンク

まとめ

今回は、Pythonでの非同期構文とモジュールasyncioとコルーチンの利用方法について学びました。

コルーチンなどの非同期処理は、Pythonのバージョンが変わるごとに処理方法も変わってくるので、最新のバージョンを使う際には注意しましょう。