33
39

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Python の async/await を徹底解説!コルーチンの仕組み

Posted at

Group182.png

Leapcell: サーバーレスWebホスティングの最適選択

Pythonの非同期プログラミング

Pythonには、コルーチン、マルチスレッディング、マルチプロセッシングなど、複数の非同期アプローチがあります。さらに、いくつかの従来の方法やサードパーティの非同期ライブラリもあります。この記事では主にコルーチンに焦点を当て、マルチスレッディングとマルチプロセッシングについても簡単に紹介します。

async/await

Pythonでは、asyncで宣言された関数は非同期関数で、しばしばコルーチンと呼ばれます。例えば:

import asyncio

async def hello():
    await asyncio.sleep(1)
    print("hello leapcell")
呼び出し方法

非同期関数の呼び出し方法は通常の関数と少し異なります。例えば、通常の関数の呼び出し方法は次の通りです:

def hello():
    print("hello leapcell")

hello()

そして、非同期関数の呼び出し方法は次の通りです:

import asyncio

async def hello():
    await asyncio.sleep(1)
    print("hello leapcell")

h = hello()
asyncio.run(h)

非同期関数を呼び出すとき、まずh = hello()を使うとコルーチンオブジェクトが返され、関数内のコードは実行されません。asyncio.run(h)関数またはawait h文を使った後で初めてコードが実行されます。次の例を見てください:

import asyncio

async def async_function():
    print("This is inside the async function")
    await asyncio.sleep(1)
    return "Async function result"

# 正しい使い方
async def correct_usage():
    print("Correct usage:")
    result = await async_function()
    print(f"Result: {result}")

# awaitを使わずに呼び出す
def incorrect_usage():
    print("\nIncorrect usage:")
    coroutine = async_function()
    print(f"Returned object: {coroutine}")
    # 注意: "This is inside the async function"はここでは表示されません

# 待機されていないコルーチンを処理する
async def handle_unawaited_coroutine():
    print("\nHandling unawaited coroutine:")
    coroutine = async_function()
    try:
        # asyncio.run()を使ってコルーチンを実行する
        result = await coroutine
        print(f"Result after handling: {result}")
    except RuntimeWarning as e:
        print(f"Caught warning: {e}")

async def main():
    await correct_usage()
    incorrect_usage()
    await handle_unawaited_coroutine()

asyncio.run(main())

非同期関数の一般的な呼び出し方法

asyncio.gather()

gatherを使うと、複数のタスクを同時に開始し、並行して実行できます。すべてのタスクが完了して結果が返された後、その後のコードが続けて実行されます。例えば:

import asyncio

async def num01():
    await asyncio.sleep(1)
    return 1

async def num02():
    await asyncio.sleep(1)
    return 2

async def combine():
    results = await asyncio.gather(num01(), num02())
    print(results)

asyncio.run(combine())

出力:

[1, 2]

上記には2つの非同期関数があります。ここでasyncio.gatherを使ってこれら2つの関数を並行して実行し、awaitを使って結果を待ちます。返された結果はresultsに格納されます。

直接awaitを使う

上記のgatherメソッドは複数の非同期関数をまとめて並行して実行します。この方法以外にも、直接awaitキーワードを使うこともできます。次の例を見てください:

import asyncio

async def hello():
    await asyncio.sleep(1)
    return "hello leapcell"

async def example():
    result = await hello()
    print(result)

asyncio.run(example())

出力:

hello leapcell

上記のexample関数では、awaitを使って非同期関数の結果を待ちます。結果が返された後、それをコンソールに表示します。この方法は実際には逐次的に実行されます。なぜなら、コードはawait文で結果が返されるまで待機し、その後のコードを続けて実行するからです。

ここで待たない場合はどうなるでしょうか?result = hello()を使うと、hello()内のコードは実行されず、返されるresultはコルーチンオブジェクトになります。

asyncio.create_task()

上記の方法以外に、もっと柔軟な方法があります。それはasyncio.create_task()を使う方法です。このメソッドはタスクを作成し、すぐにバックグラウンドで実行します。このとき、メイン関数は他の操作を行うことができます。非同期タスクの結果を取得する必要があるときは、awaitを使って取得します。次の例を見てください:

import asyncio

async def number():
    await asyncio.sleep(1)
    return 1

async def float_num():
    await asyncio.sleep(1)
    return 1.0

async def example():
    n = asyncio.create_task(number())
    f = asyncio.create_task(float_num())
    print("do something...")

    print(await n)
    print(await f)

asyncio.run(example())

出力:

do something...
1
1.0

上記の出力からわかるように、create_taskはまずタスクを作成して開始します。このとき、メイン関数はブロックされず、その後のコードを続けて実行します。非同期関数の結果が必要になったら、await nを呼び出して結果を取得します。このように、時間のかかるタスクを非同期コードに入れて実行し、必要なときにこれらの非同期関数の結果を取得することができます。

注意: create_taskを使って非同期関数を呼び出す方法は、通常の関数の呼び出し方法とは異なります。通常の関数呼び出し方法number()を使うと、関数は実行されません。しかし、create_taskを使って非同期関数を呼び出すと、関数はすぐに実行されます。awaitを使って結果を取得しなくても、関数はメイン関数が終了する前に実行を完了します。

セマフォ

asyncio.SemaphoreはPythonのasyncioライブラリにある同期プリミティブで、共有リソースへのアクセスを制御します。非同期プログラミングで非常に役立ち、特定のリソースに同時にアクセスできるコルーチンの数を制限できます。次のコードを見てください:

import asyncio
import aiohttp

async def fetch(url, session, semaphore):
    async with semaphore:
        print(f"Fetching {url}")
        async with session.get(url) as response:
            return await response.text()

async def main():
    urls = [
        "http://example.com",
        "http://example.org",
        "http://example.net",
        "http://example.edu",
        "http://example.io",
    ]

    semaphore = asyncio.Semaphore(2)  # 同時リクエスト数を2に制限
    async with aiohttp.ClientSession() as session:
        tasks = [fetch(url, session, semaphore) for url in urls]
        responses = await asyncio.gather(*tasks)

    for url, response in zip(urls, responses):
        print(f"URL: {url}, Response length: {len(response)}")

asyncio.run(main())

上記のコードでは、asyncio.Semaphore(2)を作成して同時リクエスト数を2に制限しています。非同期関数fetchでは、async with semaphoreを使ってセマフォを取得および解放しています。入る前には自動的にacquire()メソッドが呼び出されてセマフォを取得し、withブロックを抜けるときにrelease()メソッドが呼び出されてセマフォを解放します。Semaphoreを使うことで、同時リクエスト数を制御でき、サーバーに過度の負荷をかけることを防ぐことができます。データベース接続などの制限されたリソースを扱うときに非常に役立ちます。同時に、システムパフォーマンスを最適化し、並行性のバランスを見つけることができます。

セマフォの原理

内部的にはカウンターを維持しています。カウンターがゼロより大きいときはアクセスが許可され、ゼロに等しいときはアクセスが禁止されます。カウンターを取得および解放するメソッドはそれぞれacquire()release()です。初期化時に初期カウンター値を指定する必要があります。その後、コード内でカウンター値を制御することで、同時リクエスト数を制御します。

マルチスレッディング

マルチスレッディングは従来の並行タスク実行方法で、I/Oバウンドタスクに適しています。次の例を見てください:

import threading
import time

def worker(name):
    print(f"Worker {name} starting")
    time.sleep(2)  # 時間のかかる操作をシミュレート
    print(f"Worker {name} finished")

def main():
    threads = []
    for i in range(3):
        t = threading.Thread(target=worker, args=(i,))
        threads.append(t)
        t.start()

    for t in threads:
        t.join()

    print("All workers finished")

if __name__ == "__main__":
    main()

マルチスレッディングのt.join()は、3つのスレッドの完了を待つために使われます。スレッドまたはプロセスオブジェクトでjoin()メソッドを呼び出すと、呼び出し元のスレッド(通常はメインスレッド)は、join()が呼び出されたスレッドまたはプロセスが実行を完了するまでブロックされます。

マルチプロセッシング

マルチプロセッシングはCPUバウンドタスクに適しており、マルチコアプロセッサを最大限に活用できます。次の例を見てください:

import multiprocessing
import time

def worker(name):
    print(f"Worker {name} starting")
    time.sleep(2)  # 時間のかかる操作をシミュレート
    print(f"Worker {name} finished")

if __name__ == "__main__":
    processes = []
    for i in range(3):
        p = multiprocessing.Process(target=worker, args=(i,))
        processes.append(p)
        p.start()

    for p in processes:
        p.join()

    print("All workers finished")

まとめ

上記の非同期方法以外にも、Pythonには他の非同期アプローチがあります。例えば、コールバック関数を使う方法やGeventなどのサードパーティライブラリを使う方法です。それぞれの方法には独自の利点と制限があります。例えば、スレッドはI/Oバウンドタスクに適していますが、GIL(グローバルインタープリターロック)の制限を受けます。マルチプロセッシングはCPUバウンドタスクに適していますが、メモリオーバーヘッドが大きいです。サードパーティライブラリは特殊な機能や最適化を提供しますが、プロジェクトの複雑さを増す可能性があります。これに対して、async/await構文はより現代的で読みやすい非同期プログラミング方法を提供しており、現在Pythonで非同期操作を扱うために推奨される方法です。

Leapcell: サーバーレスWebホスティングの最適選択

最後に、Pythonサービスのデプロイに最適なプラットフォームを紹介します:Leapcell

brandpic7.png

🚀 好きな言語で構築

JavaScript、Python、Go、またはRustで簡単に開発できます。

🌍 無制限のプロジェクトを無料でデプロイ

使用した分だけ料金がかかります。リクエストがなければ料金はかかりません。

⚡ 従量課金制、隠れた費用は一切ありません

アイドル料金はなく、スムーズなスケーラビリティが保証されます。

Frame3-withpadding2x.png

📖 ドキュメントを探索する

🔹 Twitterでフォローしましょう: @LeapcellHQ

33
39
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
33
39

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?