{"id":15973726,"url":"https://github.com/c-bata/concurrency-in-python","last_synced_at":"2025-06-12T11:39:30.911Z","repository":{"id":76859131,"uuid":"84577637","full_name":"c-bata/concurrency-in-python","owner":"c-bata","description":"Tutorial of concurrency in Python3 (Multi-threading, Multi-processing, Asynchronous programming)","archived":false,"fork":false,"pushed_at":"2018-09-26T05:18:32.000Z","size":1193,"stargazers_count":15,"open_issues_count":0,"forks_count":1,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-04-02T11:22:25.259Z","etag":null,"topics":["concurrency","python"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/c-bata.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2017-03-10T16:11:14.000Z","updated_at":"2019-09-12T08:56:12.000Z","dependencies_parsed_at":null,"dependency_job_id":"9797dcc8-4861-419e-8f3c-f1125932914c","html_url":"https://github.com/c-bata/concurrency-in-python","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/c-bata/concurrency-in-python","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/c-bata%2Fconcurrency-in-python","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/c-bata%2Fconcurrency-in-python/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/c-bata%2Fconcurrency-in-python/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/c-bata%2Fconcurrency-in-python/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/c-bata","download_url":"https://codeload.github.com/c-bata/concurrency-in-python/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/c-bata%2Fconcurrency-in-python/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":259456689,"owners_count":22860594,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["concurrency","python"],"created_at":"2024-10-07T21:06:41.337Z","updated_at":"2025-06-12T11:39:30.856Z","avatar_url":"https://github.com/c-bata.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# 並行処理 in Python\n\n- Author: [Masashi Shibata (@c_bata_)](https://twitter.com/c_bata_)\n- Date: 2017/03/21 (Tue)\n- Event: 神戸Pythonの会\n\n## アジェンダ\n\n1. 同期的なアプローチ\n2. マルチスレッド\n3. async/awaitによる非同期処理\n4. マルチプロセス\n5. 実践\n\n## 同期的なアプローチ\n\nまずはテーマである非同期処理の話をする前に、とあるサーバに複数のHTTPリクエストを送る例を見てみましょう。\n\n### サーバーのセットアップ\n\n今からとあるサーバにいくつかHTTPのリクエストを送ってみます。\n実際にどこかのサービスのAPIとかを叩いてみてもいいんですが、\nあまり負荷をかけるのも迷惑なのでサーバを用意しますね。\n\n```python\nimport time\n\ndef app(environ, start_response):\n    time.sleep(1)\n    start_response('200 OK', [('Content-type', 'text/plain; charset=utf-8')])\n    return [b'This is a slow web api']\n```\n\nワーカー数を3つで動かしてみましょう。\n\n```console\n$ gunicorn -w 3 server:app\n```\n\n### クライアントを書いてみる\n\nそれではいくつかHTTPリクエストを送ってみます。\nPythonでは、requestsという有名なパッケージがあるので、こちらを使ってみましょう。\n\n```python\nimport requests\n\ndef main():\n    urls = ['http://localhost:8000' for _ in range(3)]\n    for u in urls:\n        r = requests.get(u)\n        print(r.text)\n\nif __name__ == '__main__':\n    main()\n```\n\n実行してみます。\n\n```console\n$ time python client_sync.py \nThis is a slow web api\nThis is a slow web api\nThis is a slow web api\n\nreal    0m3.240s\nuser    0m0.164s\nsys     0m0.037s\n```\n\n3秒ちょっとかかりました。\n今回用意したサーバは、レスポンスを返すのに1秒かかるので、ごく自然な結果ですね。\nそれでは時間を短縮する方法を考えてみましょう。\n\n## マルチスレッド\n\nサーバからのレスポンスを待っている間、先程のPythonのプログラムはCPUを使っていません。\nこれは少し無駄なように思えますね。\n複数のスレッドを使うことで、次のように効率化できそうです。\n\n![multi-thread](./img/multi-thread.png)\n\nそれではマルチスレッドを用いて、高速化してみましょう。\n\n```python\nimport requests\nfrom threading import Thread\nfrom queue import Queue\n\ndef fetch(url, results_queue):\n    resp = requests.get(url)\n    results_queue.put(resp.text)\n\ndef main():\n    results_queue = Queue()\n\n    threads = []\n    urls = ['http://localhost:8000' for _ in range(3)]\n    for u in urls:\n        thread = Thread(target=fetch, args=[u, results_queue])\n        thread.start()\n        threads.append(thread)\n\n    while threads:\n        threads.pop().join()\n\n    while not results_queue.empty():\n        print(results_queue.get())\n\nif __name__ == '__main__':\n    main()\n```\n\nいくつか深刻な問題はありそうですが、マルチスレッドを使って並行にリクエストを送信するようにしてみました。\nGILの制約があるため、Pythonにおけるマルチスレッドを行っても1つのプロセッサコアしか利用できませんが、\nI/O待ちなどの処理ではGILが解放されるため、その間に別のスレッドがプロセッサコアを使うことができます。\n\n\n```console\n$ time python client_threading.py \nThis is a slow web api\nThis is a slow web api\nThis is a slow web api\n\nreal    0m1.199s\nuser    0m0.167s\nsys     0m0.028s\n```\n\n処理時間は1/3程度になり、非常に高速になりました。\n念のため、[PyCharm](https://www.jetbrains.com/help/pycharm/2016.3/thread-concurrency-visualization.html)でスレッドの動きを見てみましょう。\n\n**マルチスレッド化する前**\n\n![同期版](./img/fetch_sync.png)\n\n**マルチスレッド化した後**\n\n![マルチスレッドでのグラフ](./img/fetch_threading.png)\n\n目標どおりの動きをしていそうです。\n``threading`` モジュールを使って、高速に処理することができました。\n\nしかし、この実装にはいくつか問題があります。\n\n- URLの数を増やすとどうなるでしょうか？\n- タイムアウトしてしまった場合は、どうなるでしょうか？\n- 使用制限のあるAPIへのリクエストはどのようにすればいいでしょうか？\n\nマルチスレッドのプログラムでこれらのことをコントロールするのは非常に難しいです。\n\n\n## async/awaitによる非同期処理\n\n次は今回のテーマである非同期プログラミングを体験してみましょう。\n\n```python\nimport aiohttp\nimport asyncio\n\nasync def fetch(l, url):\n    async with aiohttp.ClientSession(loop=l) as session:\n        async with session.get(url) as response:\n            return await response.text()\n\n\nasync def main(l, url, num):\n    tasks = [asyncio.ensure_future(fetch(l, url)) for _ in range(num)]\n    return await asyncio.gather(*tasks)\n\n\nif __name__ == '__main__':\n    loop = asyncio.get_event_loop()\n    results = loop.run_until_complete(main(loop, 'http://localhost:8000', 3))\n    for r in results:\n        print(r)\n```\n\n解説は後回しにして、とりあえず実行してみましょう。\n\n```console\n$ time python client_async.py \nThis is a slow web api\nThis is a slow web api\nThis is a slow web api\n\nreal    0m1.415s\nuser    0m0.333s\nsys     0m0.051s\n```\n\n**1.415s** で済みました。\n``multithreading`` モジュールを使った例よりも少し遅いですが、3秒かかっていたことを考えると大幅に速くなっています。\n\n**非同期版**\n\n![非同期版](./img/fetch_async.png)\n\nasync/await により定義したものは、関数ではなくコルーチンと呼ばれます。\nこのコルーチンは asyncio モジュールに実装されたイベントループによって実行されています。\nこのとき本来はOSのスレッドを生成する必要はありません。イベントループの実行スレッドが一つ存在すればいいからです。\nある意味Pythonインタープリター上でスレッドのような仕組みを実装しているとも考えられます。\nそこでOSのスレッドと区別するために軽量スレッドやグリーンスレッドとよばれます。\n\nしかしここで疑問に上がるのが、PyCharmのコンカレンシーグラフに着目すると本来生成する必要がなかったスレッドが生成されていることがきになります。\nこれに関する調査結果は、 [asyncioがPOSIXスレッドを使っている原因を調べる](https://nwpct1.hatenablog.com/entry/asyncio-posix-threads) にまとめました。興味のある方はチェックしてみてください。\n\n### 多くのリクエストを送ってみる\n\n9回ほどリクエストを送ってみます。\n``multithreading`` を使った先程の実装で9回のリクエストを送るとこのようになります。\n\n![semaphoreなしのリクエスト](./img/many_requests_without_semaphore.png)\n\nサーバのworker数は3つなので、9個中6個のリクエストは前の処理が完了するのを待ちます。\n1, 2秒待つプロセスがあるようです。これではサーバの負荷は大きくなります。\n\nサーバを配慮して、3つずつリクエストを送りましょう。\nこれは少しややこしそうですが、 ``asyncio`` では、それを簡単に実装するSemaphoreというクラスが用意されています。\n書き換えると次のようになります。\n\n```python\nimport aiohttp\nimport asyncio\n\nasync def fetch(l, url):\n    async with aiohttp.ClientSession(loop=l) as session:\n        async with session.get(url) as response:\n            return await response.text()\n\nasync def bound_fetch(semaphore, l, url):\n    async with semaphore:\n        return await fetch(l, url)\n\nasync def main(l, url, num):\n    s = asyncio.Semaphore(3)\n    tasks = [asyncio.ensure_future(bound_fetch(s, l, url))\n             for _ in range(num)]\n    return await asyncio.gather(*tasks)\n\nif __name__ == '__main__':\n    loop = asyncio.get_event_loop()\n    results = loop.run_until_complete(main(loop, 'http://localhost:8000', 9))\n    for r in results:\n        print(r)\n```\n\n実行結果はこのようになりました。\n\n```console\n$ time python client_async_with_semaphore.py \nThis is a slow web api\nThis is a slow web api\n : (中略)\n\nreal    0m3.375s\nuser    0m0.318s\nsys     0m0.050s\n```\n\nグラフも確認してみましょう。\n\n![Semaphoreを使ったリクエスト](./img/many_requests_with_semaphore.png)\n\n\n## マルチプロセス\n\n複数のプロセスを使って並列に処理することもできます。\n今回のプログラムは、I/OバウンドでCPUはそれほど使っていないですが、GILのあるPythonにおけるCPUバウンドな処理では\nマルチプロセスを使ったアプローチは特に有効です。\n\n![I/O待ちをうまく活用する](./img/multi-process.png)\n\n```python\nimport requests\nfrom multiprocessing import Pool\n\n\ndef fetch(url):\n    resp = requests.get(url)\n    return resp.text\n\n\ndef main():\n    urls = ['http://localhost:8000' for _ in range(3)]\n    with Pool(processes=3) as pool:\n        results = pool.map(fetch, urls)\n\n    for r in results:\n        print(r)\n\nif __name__ == '__main__':\n    main()\n```\n\n``multiprocessing`` モジュール提供する ``Pool`` クラスは、\n複数のプロセスワーカーを管理する際に面倒なことを全て負担してくれています。コードは非常にシンプルで保守も簡単です。\n\n```console\n$ time python client_multiprocessing.py \nThis is a slow web api\nThis is a slow web api\nThis is a slow web api\n\nreal    0m1.347s\nuser    0m0.234s\nsys     0m0.079s\n```\n\n今回のプログラムはほとんどがI/Oで、GILによる制約はあまりありません。\n子プロセスの生成には、メモリ空間のコピーなどでスレッドの生成に比べオーバーヘッドがかかります。\nそのため実行時間は、マルチスレッドを用いたプログラムよりも長くなってしまいました。\n\nたくさん、プロセスを生成すると、メモリもたくさん消費するでしょう。\nもし並行処理が必要になった場合には、これらのメリットやデメリットを踏まえて適切なアプローチをとる必要がありそうです。\n\n\n## 非同期処理の関連資料\n\n- [You Might Not Want Async (in Python) - PyCon JP 2015](https://www.youtube.com/watch?v=IBA89nFEQ8U)\n    - 関数をコルーチンに変えた場合、呼び出し側もawaitするように書き換えないと壊れてしまう点\n    - テストの難しさ (コルーチンやイベントループの扱い)\n- [David Beazley - Python Concurrency From the Ground Up: LIVE! - PyCon 2015](https://www.youtube.com/watch?v=MCs5OvhV9S4)\n    - GILやマルチスレッド、マルチプロセスの解説\n    -　async/awaitの構文のネイティブコルーチンではなく、ジェネレータベースのコルーチンの実装\n    - タスクキューの実装\n- [I don't understand Python's Asyncio - Armin Ronacher](http://lucumr.pocoo.org/2016/10/30/i-dont-understand-asyncio/)\n\n## 実践\n\n実際に動かしてみましょう。何かわからないことがあれば質問ください。\n余力のある方は、multithreadingやmultiprocessingモジュールを使って書いてみるといいと思います\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fc-bata%2Fconcurrency-in-python","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fc-bata%2Fconcurrency-in-python","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fc-bata%2Fconcurrency-in-python/lists"}