非同期プログラミングの徹底解説 – asyncioの使い方と実践例【Python】

IT関連

はじめに

この記事でわかること

  • 非同期プログラミングとは何か、そのメリットと用途
  • Pythonのasyncioの内部構造とイベントループの仕組み
  • async / await の基本的な使い方と注意点
  • asyncioを使った並行処理の実践例
  • 同期処理とのパフォーマンス比較
  • 実際のアプリケーションでの応用例(Webスクレイピングやネットワーク通信)

1. 非同期プログラミングとは?

非同期プログラミングは、複数の処理を並行して実行できる手法です。通常の(同期的な)プログラムは、一つの処理が終わるまで次の処理を開始できません。一方、非同期プログラムでは、時間のかかる処理(例えばネットワーク通信やファイル操作)を待つ間に、他の処理を進めることができます。

非同期のメリット

  • 効率的なCPU利用: I/O待ちの時間を他の処理に活用
  • レスポンスの向上: サーバーアプリケーションなどで、リクエストを効率的に処理
  • スケーラビリティ: 多数の接続をさばく必要があるアプリケーションに適用可能

次に、Pythonのasyncioを用いた非同期処理の基本を解説します。

2. asyncioの基本 – イベントループとタスク

Pythonのasyncioは、非同期プログラミングをサポートする標準ライブラリです。asyncioの中心的な概念は以下の3つです。

  1. イベントループ: 非同期タスクを管理し、スケジューリングする役割
  2. コルーチン(coroutine): async / await を使用する非同期関数
  3. タスク(Task): イベントループ上で実行されるコルーチン

基本的なasync / await の使い方

import asyncio

async def hello():
    print("Hello")
    await asyncio.sleep(1)  # 1秒待機(非同期)
    print("World")

asyncio.run(hello())

このコードでは、hello関数が1秒間待機する間に他の処理が可能になります。
実行すると、”Hello”を出力したのちに”World”が出力されます。

次に、asyncioの内部構造を詳しく見ていきましょう。

3. asyncioの内部構造

Pythonのasyncioの中核となるのが「イベントループ」です。イベントループは、非同期タスクをスケジュールし、適切なタイミングで処理を実行する仕組みです。

イベントループの仕組み

  1. タスクの登録: async関数が呼ばれると、コルーチンオブジェクトが生成される。
  2. タスクのスケジュール: asyncio.create_task() でタスクがスケジュールされる。
  3. イベントループの実行: asyncio.run() により、イベントループが開始され、登録されたタスクが順番に実行される。

タスクの実行例

import asyncio

async def task1():
    print("Task 1 start")
    await asyncio.sleep(2)  # 2秒待機(非同期)
    print("Task 1 end")

async def task2():
    print("Task 2 start")
    await asyncio.sleep(1)  # 1秒待機(非同期)
    print("Task 2 end")

async def main():
    t1 = asyncio.create_task(task1())  # タスク1をスケジュール
    t2 = asyncio.create_task(task2())  # タスク2をスケジュール
    await t1  # タスク1の完了を待つ
    await t2  # タスク2の完了を待つ

asyncio.run(main())

このコードでは、task1task2が並行して実行されます。

4. 実践: asyncio を活用した並行処理

複数の非同期タスクを並行実行することで、効率的な処理を実現できます。

import asyncio

async def fetch_data(id):
    print(f"Fetching data {id}...")
    await asyncio.sleep(2)  # 2秒待機(非同期)
    print(f"Data {id} received")

async def main():
    tasks = [fetch_data(i) for i in range(5)]  # 5つの非同期タスクを作成
    await asyncio.gather(*tasks)  # すべてのタスクを並行実行

asyncio.run(main())

5. パフォーマンス比較 – 同期 vs 非同期

同期処理と非同期処理の違いを比較します。

同期処理の例

最初に同期処理の例を見てみます。
今回実行するコードでは、2つのタスクを実行します。
各タスクは2秒を想定します。
それでは見てみましょう。

import time

def sync_task():
    print("Starting task...")
    time.sleep(2)  # 2秒待機(同期処理)
    print("Task completed")

start = time.time()
sync_task()
sync_task()
print(f"Sync execution time: {time.time() - start:.2f} seconds")

このコードを実行してみます。
2つのタスクを同期(1つずつ順番)処理しているため、4秒(2秒のタスク×2回)かかっています。

非同期処理の例

次に、非同期処理で試してみます。

import asyncio
import time

async def async_task():
    print("Starting async task...")
    await asyncio.sleep(2)  # 2秒待機(非同期処理)
    print("Async task completed")

async def main():
    task1 = asyncio.create_task(async_task())
    task2 = asyncio.create_task(async_task())
    await task1
    await task2

start = time.time()
asyncio.run(main())
print(f"Async execution time: {time.time() - start:.2f} seconds")

実行結果としては、2.02秒でした。
若干の誤差はありますが、この非同期処理では、2つのタスクが並行して実行されるため、合計の処理時間が短縮されます。

6. ケーススタディ: asyncio を使ったアプリケーション

今回紹介した、asyncioモジュールを使用した非同期プログラミングの基本的な概念

1. Webスクレイピング(aiohttpとBeautifulSoupを使用)

aiohttpを使用して非同期にWebページをダウンロードし、BeautifulSoupを使用してHTMLを解析します。 複数のURLを同時にスクレイピングすることで、処理時間を短縮します。

import asyncio
import aiohttp
from bs4 import BeautifulSoup

async def fetch_url(session, url):
    async with session.get(url) as response:
        return await response.text()

async def scrape_data(url):
    async with aiohttp.ClientSession() as session:
        html = await fetch_url(session, url)
        soup = BeautifulSoup(html, 'html.parser')
        # ここでBeautifulSoupを使用してデータを抽出
        title = soup.title.string
        return title

async def main():
    urls = ['https://www.example.com', 'https://www.example.org', 'https://www.example.net']
    tasks = [asyncio.create_task(scrape_data(url)) for url in urls]
    results = await asyncio.gather(*tasks)
    for result in results:
        print(result)

if __name__ == '__main__':
    asyncio.run(main())

2. API通信(aiohttpを使用)

aiohttpを使用して非同期にAPIリクエストを送信し、JSON形式のレスポンスを処理します。 複数のAPIリクエストを同時に送信することで、効率的なデータ取得を実現します。

import asyncio
import aiohttp
import json

async def fetch_api(session, url):
    async with session.get(url) as response:
        return await response.json()

async def main():
    api_url = 'https://jsonplaceholder.typicode.com/posts/1'
    async with aiohttp.ClientSession() as session:
        data = await fetch_api(session, api_url)
        print(json.dumps(data, indent=2))

if __name__ == '__main__':
    asyncio.run(main())

3. 非同期Webサーバー(aiohttpを使用)

aiohttp.webを使用して非同期Webサーバーを構築します。 複数のクライアントからのリクエストを同時に処理し、高いパフォーマンスを実現します。

import asyncio
from aiohttp import web

async def handle(request):
    name = request.match_info.get('name', "Anonymous")
    text = "Hello, " + name
    return web.Response(text=text)

async def main():
    app = web.Application()
    app.add_routes([web.get('/', handle),
                    web.get('/{name}', handle)])
    runner = web.AppRunner(app)
    await runner.setup()
    site = web.TCPSite(runner, 'localhost', 8080)
    await site.start()

    print("Server started at http://localhost:8080")

    # サーバーを永続的に実行
    await asyncio.Event().wait()

if __name__ == '__main__':
    asyncio.run(main())

7. まとめ

非同期プログラミングは、I/O待ち時間を効率的に活用し、アプリケーションのパフォーマンスを向上させます。asyncioの内部構造を理解し、適切に活用することで、より高速でスケーラブルなアプリケーションを開発できます。

コメント

PAGE TOP
タイトルとURLをコピーしました