Pythonにおけるマルチスレッドプログラミングの基礎と実践:効率的な並列処理を学ぶ
![]()
【Yuki】
Hiroki君、こんにちは。今日はPythonの「マルチスレッド」という技術について一緒に学んでいこうと思います……。少し複雑な概念に聞こえるかもしれませんが、プログラミングを効率化するためにはとても大切な技術なんです。
![]()
【Hiroki】
Yukiさん、よろしくお願いします!マルチスレッドって、名前だけは聞いたことがあります。CPUをフル活用して、複数の仕事を同時に片付けるようなイメージですよね?難しそうですけど、頑張って覚えたいです。
![]()
【Yuki】
そうですね、そのイメージで合っています。ただ、Python特有の事情もあって、実は少し「癖」があるんです……。まずは、基本的な概念から説明していきますね。
1. マルチスレッドとは何か
![]()
【Yuki】
まず、「スレッド(Thread)」というのは、プログラムを実行する際の最小単位のことです。通常、プログラムは上から下へと順番に一つずつ命令をこなしていきます。これを「シングルスレッド」と呼びます。
![]()
【Hiroki】
一本の道に沿って進むような感じですね。
![]()
【Yuki】
はい、その通りです。対して「マルチスレッド」は、一つのプログラムの中で複数の道(スレッド)を同時に走らせる仕組みのことです。例えば、大きなファイルをダウンロードしながら、同時に画面上のボタンを操作できるようにしたり……そういった「待ち時間」を有効活用するために使われます。
![]()
【Hiroki】
なるほど。もしシングルスレッドだったら、ダウンロードが終わるまで画面が固まっちゃうってことですか?
![]()
【Yuki】
ええ、そうです。ユーザーからすると、アプリが反応しなくなったように見えて不安になりますよね。そういったことを防ぐために、バックグラウンドで重い処理を走らせるのがマルチスレッドの主な役割なんです。
2. Pythonでスレッドを作ってみる
![]()
【Yuki】
では、実際にPythonでどうやってスレッドを作るのか、簡単なコードを見てみましょう。Pythonには標準で threading というライブラリが用意されています。
import threading
import time
def slow_task(name):
print(f"{name}:処理を開始します...")
time.sleep(3) # 3秒間待機する処理
print(f"{name}:処理が完了しました!")
# メインの処理
print("プログラムを開始します。")
# スレッドの作成
thread1 = threading.Thread(target=slow_task, args=("スレッドA",))
thread2 = threading.Thread(target=slow_task, args=("スレッドB",))
# スレッドの開始
thread1.start()
thread2.start()
# スレッドが終わるのを待つ
thread1.join()
thread2.join()
print("すべての処理が終了しました。")
![]()
【Hiroki】
お、これだけでいいんですか? threading.Thread を作って、 start() を呼ぶだけ。
![]()
【Yuki】
そうです。意外とシンプルに書けると思います……。ここで重要なのは start() で処理を開始した直後、メインのプログラムは次の行へ進んでしまうということです。だから、プログラムの最後で join() を呼んで、「スレッドが終わるまで待ってね」と伝えているんです。
![]()
【Hiroki】
もし join() がなかったらどうなるんですか?
![]()
【Yuki】
その場合は、スレッドが裏で動いている間に、メインの処理が「すべての処理が終了しました」と表示して、先に終わってしまうかもしれません……。それは少し、不格好ですよね。
3. CPUバウンドとI/Oバウンドの違い
![]()
【Hiroki】
マルチスレッドを使えば、どんな計算も2倍、3倍と速くなるんでしょうか?
![]()
【Yuki】
……実は、そこがPythonの難しいところなんです。マルチスレッドが効果を発揮するのは、主に「I/Oバウンド(入出力待ち)」と呼ばれる処理です。
![]()
【Hiroki】
アイオーバウンド……?
![]()
【Yuki】
はい。例えば「インターネットからデータを取ってくる」「ファイルを読み書きする」「データベースの返答を待つ」といった、プログラム自体が計算しているのではなく、「外からの返事を待っている状態」のことです。
![]()
【Hiroki】
なるほど、待ち時間が多い処理のことですね。
![]()
【Yuki】
一方で、複雑な数学の計算や画像処理など、CPUをずっと使い続ける処理は「CPUバウンド」と呼ばれます。実は、Pythonではマルチスレッドを使っても、CPUバウンドな処理を劇的に速くすることはできないんです……。
![]()
【Hiroki】
えっ、そうなんですか?せっかく複数のスレッドがあるのに、どうしてですか?
4. Pythonの壁「GIL(グローバル・インタプリタ・ロック)」
![]()
【Yuki】
それは、Python(特に一般的なCPython)には GIL(Global Interpreter Lock) という仕組みがあるからです。これは、「一度に一つのスレッドしかPythonの命令を実行させない」という強力なロック(鍵)のようなものです。
![]()
【Hiroki】
ええーっ! じゃあ、スレッドをたくさん作っても、実際には順番にしか動いていないってことですか?
![]()
【Yuki】
残念ながら、その通りなんです……。複数のスレッドがあったとしても、CPUでの計算処理は交代で少しずつ実行されています。ただし、さっき言った「待ち時間(I/Oバウンド)」が発生している間は、そのスレッドは鍵を離してくれるので、別のスレッドが動くことができます。
![]()
【Hiroki】
だから、待ち時間が多い通信処理などには有効だけど、計算そのものを並列化して速くするのは苦手なんですね。
![]()
【Yuki】
よく理解できましたね、Hiroki君。もし、CPUバウンドな処理を並列化して高速化したい場合は、スレッドではなく「マルチプロセス(multiprocessing)」という、別の仕組みを使う必要があります。でも、今日はまずスレッドの方をしっかり覚えましょう。
5. スレッドセーフと「Lock」の重要性
![]()
【Yuki】
マルチスレッドを使う際に、一番気をつけなければいけないのが「データの共有」です。複数のスレッドが同時に同じ変数を書き換えようとすると、データが壊れてしまうことがあるんです……。これを防ぐために、「Lock(ロック)」という仕組みを使います。
![]()
【Hiroki】
データが壊れる……。なんだか怖いですね。具体的にどんなことが起きるんですか?
![]()
【Yuki】
例えば、銀行の口座残高を操作するような処理を想像してみてください。
import threading
balance = 100 # 残高100円
lock = threading.Lock()
def deposit(amount):
global balance
# ロックを取得する
with lock:
current = balance
# ここで少し時間がかかると想定
new_balance = current + amount
balance = new_balance
# 100円預けるスレッドをたくさん作る
threads = []
for i in range(10):
t = threading.Thread(target=deposit, args=(100,))
threads.append(t)
t.start()
for t in threads:
t.join()
print(f"最終残高: {balance}円")
![]()
【Yuki】
このコードの with lock: という部分が大切です。これがあるおかげで、あるスレッドが計算している最中は、他のスレッドが balance を触ることができなくなります。
![]()
【Hiroki】
もしその with lock: がなかったら、複数のスレッドが同時に「今の残高は100円だな」と読み取ってしまって、みんなが100円を足して合計200円になってしまう……みたいな計算ミスが起きる可能性があるんですね。
![]()
【Yuki】
その通りです。これを「競合状態(レースコンディション)」と呼びます。マルチスレッドを扱うときは、常に「今、複数の場所から同じデータを触っていないか?」ということを意識しなければいけません。控えめな性格の私でも、こういう時はしっかり「鍵」をかけて管理しないと、大きなトラブルになってしまいますから……。
6. 実践的な使い方:ThreadPoolExecutor
![]()
【Hiroki】
スレッドの作り方はわかりました。でも、たくさんのスレッドを手動で start() したり join() したりするのは、ちょっと大変そうですね。
![]()
【Yuki】
そうですね。最近のPythonでは、もっとスマートにスレッドを管理できる concurrent.futures.ThreadPoolExecutor というクラスを使うのが一般的です。これを使うと、スレッドの使い回し(プール)を自動でやってくれます。
from concurrent.futures import ThreadPoolExecutor
import time
def fetch_data(url):
print(f"{url} のデータを取得中...")
time.sleep(1) # ネットワーク通信の代わり
return f"{url} のデータ完了"
urls = ["site-A.com", "site-B.com", "site-C.com", "site-D.com"]
# 最大3つのスレッドで並列実行
with ThreadPoolExecutor(max_workers=3) as executor:
results = list(executor.map(fetch_data, urls))
for res in results:
print(res)
![]()
【Hiroki】
わあ、こっちの方がずっとスッキリしていますね! max_workers で同時に動く数を制限できるのも便利そうです。
![]()
【Yuki】
そうなんです。スレッドを無限に作ってしまうと、パソコンのメモリをたくさん消費してしまいます。こうして「同時に動く数を決めて、手の空いたスレッドに次の仕事を任せる」というやり方が、とても効率的なんです……。
7. まとめと注意点
![]()
【Hiroki】
今日はマルチスレッドについてたくさん教えてもらいました。
1. threading モジュールで複数の処理を並列に動かせる。
2. Pythonには GIL があるので、主にI/Oバウンド(待ち時間が多い処理)に有効。
3. Lock を使って、データの整合性を守る必要がある。
4. ThreadPoolExecutor を使うと管理が楽になる。
……こんな感じでしょうか?
![]()
【Yuki】
完璧なまとめです、Hiroki君。素晴らしいと思います……。
最後に、マルチスレッドを使うときの注意点として、「デバッグが難しくなる」ということを覚えておいてください。シングルスレッドなら順番に実行されるので間違いを見つけやすいですが、スレッドはバラバラのタイミングで動くので、たまにしか起きない不具合が発生したりすることもあります。
![]()
【Hiroki】
なるほど。便利だけど、慎重に使わないといけない道具なんですね。
![]()
【Yuki】
はい。でも、ネットワーク通信を多用するプログラムや、ユーザーの操作感を高めたいときには必ず役に立ちます。ぜひ、小さなツールを作る際に試してみてくださいね。
![]()
【Hiroki】
はい!まずは簡単なスクレイピングとか、ファイルのコピーなんかで試してみようと思います。Yukiさん、ありがとうございました!
![]()
【Yuki】
……どういたしまして。あ、最後にいくつか参考になるリンクを置いておきますね。もしもっと深く知りたくなったら、夜の静かな時間にでも読んでみてください……。その方が、きっと集中できると思いますから。
参考リンク: - Python 3.12 公式ドキュメント: threading --- スレッドベースの並列処理 - Python 3.12 公式ドキュメント: concurrent.futures --- 並列タスク実行 - Real Python: An Intro to Threading in Python (英語)
![]()
【Yuki】
マルチスレッドの基本はここまでです。次は、計算処理を速くするための「マルチプロセス」についても、また別の機会にお話しできれば嬉しいです……。お疲れ様でした。
この記事では基礎を解説しましたが、実務においては「もっと複雑なデータを扱いたい」「独自のシステムに組み込みたい」といった、個別の課題に直面することも多いはずです。
「自分で書く時間は最小限に抑え、プロの品質でツールを完成させたい」という方は、ぜひ一度ご相談ください。
- 専門家の知見に基づいた、保守性の高いコード設計
- AIの専門家による、Gemini API等の最新AIを組み合わせた高度な自動化
- ChatGPT等が生成したコードのデバッグ・最適化
「教わる」だけでなく「形にする」パートナーとして、フリーランスエンジニアのmei_13が最短ルートでの解決をサポートします。


