はじめに
Python 3.14のリリースを2025年10月7日に控え、バージョン3.14.0rc3が9月18日にリリースされました。
What's newドキュメントでは多くの新機能が紹介されていますが、私が特に注目しているのは『PEP 779: Free-threaded Python is officially supported』です。Python 3.13でもオプションとしてFree-threadingが実験的*1にサポートされていましたが、Python 3.14では正式にサポートされます。
Free-threadingによってGlobal Interpreter Lock(GIL)を無効化できるようになると、マルチスレッドのアプリケーションが真にマルチコアCPUの恩恵を受けることができます。つまり、マルチスレッドはI/Oバウンドなタスクの並行処理に限らず、CPUバウンドなタスクの並列処理にとっても有力な選択肢となりました。
本記事では、Free-threaded Pythonの入門から実践まで、実例を通して徹底的に解説します。レベルを問わず、並列処理に興味がある全てのPythonistaを対象読者としています。
本記事の内容は、2025年9月20日時点の情報に基づいています。
- はじめに
- 1. Pythonにおける並行処理と並列処理
- 2. Free-threaded Pythonの概要とGIL
- 3. Free-threaded Pythonのベンチマーク
- 4. Free-threaded Pythonのプロファイリング
- 5. Free-threaded Pythonの活用事例
- 6. threadingの落とし穴
- まとめ
1. Pythonにおける並行処理と並列処理
まず、並行処理(Concurrency)と並列処理(Parallelism)について復習します。初学者はよくこれらを混同してしまいがちですが、全く異なる概念です。
本記事の読者はこれらの概念を大まかに理解していると予想されるため、Go言語の共同開発者の一人であるRob Pike氏の『Concurrency is not parallelism』という素晴らしい講演からの引用に留めます。「処理する(dealing with)」と「実行する(doing)」という言葉の対比によって、並行処理と並列処理の違いがとても美しく説明されています。
In programming, concurrency is the composition of independently executing processes, while parallelism is the simultaneous execution of (possibly related) computations. Concurrency is about dealing with lots of things at once. Parallelism is about doing lots of things at once.
FastAPIの公式ドキュメントでは、ハンバーガーショップの例を用いて並行処理と並列処理の違いが分かりやすく紹介されているので、こちらもぜひ参考にしてください。
次に、プロセス(Process)とスレッド(Thread)について復習します。釈迦に説法かもしれませんが、以下の説明はMicrosoft Learnの『Processes and Threads』より引用したものです。
An application consists of one or more processes. A process, in the simplest terms, is an executing program. One or more threads run in the context of the process. A thread is the basic unit to which the operating system allocates processor time. A thread can execute any part of the process code, including parts currently being executed by another thread.
プロセスは実行中のプログラムそのものであり、スレッドはOSがCPU時間を割り当てる基本単位です。プロセスには1つ以上のスレッドが含まれ、同じプロセスに属するスレッドはリソースを共有します。プロセスを「リソースが関連付けられる実体」、スレッドを「スケジューリングの対象となる実体」と説明することもできます。
さて、Pythonにおける並行・並列処理を実現するためのライブラリはいくつか存在しますが、ここでは代表的な標準ライブラリであるthreading/multiprocessing/asyncioの3つを紹介します。表中の取り消し線や太字は本記事のメインテーマであるFree-threaded Pythonのサポートによる変更です。
| threading | multiprocessing | asyncio | |
|---|---|---|---|
| 並行/並列 | 並列 |
並列 | 並行 |
| 実行単位 | スレッド | プロセス | コルーチン/タスク |
| GILの影響 | 受けない |
受けない | 受けない |
| 得意なタスク | I/Oバウンド CPUバウンド |
CPUバウンド | I/Oバウンド |
| メモリ空間 | 共有 | 独立 | 共有 |
| オーバーヘッド | 小 | 大 | 極小 |
| ユースケース | ファイルI/O ネットワークリクエスト ??? |
科学技術計算 データ処理 画像・動画処理 機械学習 |
Webサーバー ネットワークI/O データベース接続 |
表中のthreadingのユースケースにおける「???」については、これからPythonコミュニティ全体でFree-threaded Pythonのユースケースを模索していく必要があるというメッセージを込めています。
表を見やすさを優先した結果、やや説明不足になってしまいました。
それでは、Python公式ドキュメントを参考にそれぞれ簡単に紹介します。
1-1. threading
threadingは、プロセス内で複数のスレッドを並行に実行できるライブラリです。
import threading import time import os def worker(name: str, duration: int) -> None: print(f"thread {name} started (pid: {os.getpid()})") time.sleep(duration) print(f"thread {name} finished") if __name__ == "__main__": t1 = threading.Thread(target=worker, args=("t1", 2,)) t2 = threading.Thread(target=worker, args=("t2", 1,)) t1.start() t2.start() t1.join() t2.join() print("all threads have finished")
$ python3.13 threading_example.py thread t1 started (pid: 21808) thread t2 started (pid: 21808) thread t2 finished thread t1 finished all threads have finished
Python 3.12以前はGILの影響により、1つのスレッドしか同時にPythonコードを実行することができず、threadingでマルチコアCPUの恩恵を受けることができませんでした。そのため、CPUバウンドなタスクを実行する場合はmultiprocessingを利用し、I/Oバウンドなタスクを実行する場合のみthreadingを利用していたPythonistaも多いはずです。
一方、Free-threadingがPython 3.13で実験的にサポートされ、Python 3.14では正式にサポートされます。依然としてGILの無効化はオプションですが、threadingでも真の並列性が実現されるためユースケースは増加すると考えられます。Free-threaded PythonとGILについては、第2章でより詳しく説明します。
スレッド間はメモリ空間を共有しているためプロセスに比べてオーバーヘッドが小さく、コンテキストスイッチのコストも低いです。
メモリ空間を共有することで、簡単かつ高速にスレッド間でデータを共有することができますが、競合状態(Race Condition)やデッドロック(Deadlock)を引き起こす可能性があるためLockなどを適切に利用する必要があります。これについては第6章で詳しく取り扱います。
1-2. multiprocessing
multiprocessingは、複数のプロセスを生成して並列に実行できるライブラリです。
import multiprocessing import time import os def worker(name: str, duration: int) -> None: print(f"process {name} started (pid: {os.getpid()})") time.sleep(duration) print(f"process {name} finished") if __name__ == "__main__": p1 = multiprocessing.Process(target=worker, args=("p1", 2,)) p2 = multiprocessing.Process(target=worker, args=("p2", 1,)) p1.start() p2.start() p1.join() p2.join() print("all processes have finished")
$ python3.13 multiprocessing_example.py process p2 started (pid: 20304) process p1 started (pid: 20303) process p2 finished process p1 finished all processes have finished
multiprocessingはサブプロセスを利用しているためGILを回避することができ、マルチコアマシンの恩恵を受けることができます。threadingと似たようなAPIの他に、データ並列に便利なPoolなどもサポートしています。
multiprocessingはプロセスを開始する方法としてspawn、fork、forkserverという3つの方法をサポートしています。WindowsとmacOSのデフォルトであるspawnは新しいPythonインタプリタを起動しますが、親プロセスから実行に必要なリソースのみ引き継ぐため、他の方法に比べて起動が少し遅くなります。Python3.13以前はmacOSを除くPOSIXのデフォルトであったforkは親プロセスが複製されてリソースが全て引き継がれるため、起動は非常に高速ですが親プロセスがマルチスレッドで動作している場合は安全性に問題があります。こうした背景によりPython3.14からPOSIXのデフォルトになったforkserverは、プロセスを生成するためのサーバープロセスを予め起動し、このサーバーに依頼することで安全な状態からプロセスをforkします。いずれの方法も、if __name__ == '__main__'内でmultiprocessing.set_start_method()によって指定する必要があるので注意が必要です。
プロセスは独自のメモリ空間を持つため、プロセス間のデータ共有には共有メモリかサーバープロセスを利用する必要があります。共有メモリはValueやArrayといったクラスが利用でき、メモリ経由で直接データを共有できるためパフォーマンスは高いですが、数値や配列などC言語の単純なデータ型と互換性のあるものに限定されます。一方でサーバープロセスはManager()によってサーバープロセスがオブジェクトを管理し、各プロセスがプロキシ経由で操作します。任意のオブジェクトを扱える柔軟性がありますが、シリアライズ/デシリアライズやデータコピーが発生するため共有メモリより低速です。
multiprocessingはPyTorchやjoblib(scikit-learnのdependenciesの1つ)など、多くのPythonライブラリで並列処理を実現するために利用されています。
1-3. asyncio
asyncioは、async/await構文を利用して非同期なコードを書けるライブラリです。asyncioの基礎については、以下の公式ドキュメントが参考になります。
import asyncio import os async def worker(name: str, duration: int) -> None: print(f"task {name} started (pid: {os.getpid()})") await asyncio.sleep(duration) print(f"task {name} finished") async def main() -> None: t1 = asyncio.create_task(worker("t1", 2)) t2 = asyncio.create_task(worker("t2", 1)) await asyncio.gather(t1, t2) if __name__ == "__main__": asyncio.run(main()) print("all tasks have finished")
$ python3.13 asyncio_example.py task t1 started (pid: 24620) task t2 started (pid: 24620) task t2 finished task t1 finished all tasks have finished
コルーチン(Coroutines)について、async defで定義された関数はコルーチン関数であり、それによって呼び出されたのがコルーチンオブジェクトです。一方でタスク(Tasks)にはコルーチンをイベントループ(Event Loop)に結び付ける役割があり、asyncio.create_task()などによって作成されたタスクはすぐに実行できるように自動的にスケジューリングされます。
asyncioはシングルスレッドで動作します。これは、実行中の各タスクがCPUの制御を自発的にイベントループに返却することで他のタスクに実行機会を与えます。一方、threadingはマルチスレッドを利用してOSが強制的にコンテキストスイッチを行います(プリエンプティブマルチタスキング)。この違いから、asyncioの仕組みは協調的マルチタスキング(Cooperative Multitasking)と呼ばれています。
Pythonの公式ドキュメントによると「asyncioはI/Oバウンドで高度に構造化されたネットワークコードに向いている」とされています。例えばWebフレームワークであるFastAPIは、Starletteを土台としてasync/awaitによる非同期コードを利用しています。これにより、シングルスレッドでも多数のクライアントからのリクエストを並行処理することができ、スレッドベースのアーキテクチャと比較してオーバーヘッドやメモリ消費が少なく、非常に高いパフォーマンスを獲得しています。
1-4. その他の標準ライブラリ
concurrent.futuresは、呼び出し可能なオブジェクトを非同期実行するための高レベルなインターフェースを提供しています。
import concurrent.futures import time import os def worker(name: str, duration: int) -> None: print(f"worker {name} started (pid: {os.getpid()})") time.sleep(duration) print(f"worker {name} finished") if __name__ == "__main__": with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor: f1 = executor.submit(worker, "t1", 2) f2 = executor.submit(worker, "t2", 1) f1.result() f2.result() print("all threads have finished") with concurrent.futures.ProcessPoolExecutor(max_workers=2) as executor: f1 = executor.submit(worker, "p1", 2) f2 = executor.submit(worker, "p2", 1) f1.result() f2.result() print("all processes have finished")
$ python3.13 concurrent_futures_example.py worker t1 started (pid: 63540) worker t2 started (pid: 63540) worker t2 finished worker t1 finished all threads have finished worker p1 started (pid: 63542) worker p2 started (pid: 63543) worker p2 finished worker p1 finished all processes have finished
Executor抽象基底クラスのsubmit()メソッドは実行をスケジュールし、実行結果や状態、例外を取得できるFutureオブジェクトを返します。ThreadPoolExecutorとProcessPoolExecutorはどちらもExecutorを実装したサブクラスであり、スレッドベースかプロセスベースかの違いに関わらず、共通のインターフェースを利用することができます。また、引数のmax_workers=によって、プールの最大のスレッド・プロセス数を指定することができます。
concurrent.futures.ProcessPoolExecutorはsubmit()メソッドを基本として、ノンブロッキングなタスクの投入とブロッキングな結果の待機が明確に分離されるように設計されています。これに対し、multiprocessing.Poolは、投入と待機が一体化したブロッキングなメソッドと、_async接尾辞で区別されるノンブロッキングなメソッドが混在しており、APIの設計思想が一貫しているとは言えません。
2. Free-threaded Pythonの概要とGIL
PEP 703とそのacceptanceにおいて、Free-threaded Pythonの3段階のロールアウトフェーズが紹介されました。フェーズIはPython 3.13から始まり、Free-threadedビルドを利用可能かつ実験的にのみサポートしています。Python 3.14から始まるフェーズIIでは、Free-threadedビルドが依然としてオプションではありますが公式にサポートされます。Python 3.14が公式サポートの基準を満たすとするPEP 779とそのacceptanceは非常に大きなインパクトを与えました。そして次のフェーズⅢでは、Free-threadedビルドがデフォルトになる予定です。
PEP 779では根拠としてパフォーマンスの向上も言及されています。GILありビルドと比べてFree-threadedビルドのシングルスレッド性能低下が10%程度(macOSは3%程度)で、いくつかのPRによって10%未満に抑えられる見込みのことです。ベンチマークとしてはpyperformanceが利用されています。
そもそも、Free-threadedビルドでシングルスレッド性能が低下する原因は何でしょうか。PEP 703によると、GILなしでもCPythonをスレッドセーフにするための変更が実行時のオーバーヘッドになっているそうです。その中には、参照カウント操作のアトミック化や可変コンテナのオブジェクト単位ロック、循環GC実行時のstop-the-world同期、デッドロック回避のためのクリティカルセクション管理などが含まれます。確かにGILがなくなることでシングルスレッド性能は低下しますが、マルチスレッドで複数CPUコアを活用することで全体的なパフォーマンスは改善できます。
以下のページでFree-threadingをサポートしているパッケージのステータスを確認できます(純粋なPythonで書かれたパッケージはそのまま動作するため含まれていません)。Numpy、pandas、Pillow、PyTorch、SciPyなどの著名なライブラリが既にFree-threadingをサポートしています。
3. Free-threaded Pythonのベンチマーク
本章では、2種類のCPUバウンドタスクを通じ、Free-threaded Pythonの性能を検証します。特にGILから解放されたthreadingが、従来の並列処理の代表格であったmultiprocessingと比較して、どの程度実用的な選択肢となり得るかを考察します。
ベンチマークは、アーキテクチャが異なる以下の2つの環境で実施しました。
- MacBook Pro (OS: macOS 15.6.1, Chip: Apple M2 Pro, Cores: 10, Memory: 16 GB)
- Supermicro AS-1014S-WTRT (OS: Ubuntu 24.04.2 LTS, Chip: AMD EPYC 7543P, Cores: 32, Memory: 128 GB)
multiprocessingにおけるプロセスの開始方法は、各OSにおけるPython 3.14のデフォルト設定(macOSではspawn、Ubuntuではforkserver)をそのまま使用しています。
使用したPythonバージョンは3.14.0rc2および3.14.0rc2 free-threading buildです。
uvを利用して仮想環境のPythonバージョンを切り替える方法
$ uv venv -p 3.14 Using CPython 3.14.0rc2 Creating virtual environment at: .venv Activate with: source .venv/bin/activate $ uv python pin 3.14 Pinned `.python-version` to `3.14` $ uv run python -VV Python 3.14.0rc2 (main, Aug 28 2025, 17:02:21) [Clang 20.1.4 ] $ uv python pin 3.14t Updated `.python-version` from `3.14` -> `3.14t` $ uv venv -p 3.14t Using CPython 3.14.0rc2 Creating virtual environment at: .venv ✔ A virtual environment already exists at `.venv`. Do you want to replace it? · yes Activate with: source .venv/bin/activate $ uv run python -VV Python 3.14.0rc2 free-threading build (main, Aug 28 2025, 16:50:54) [Clang 20.1.4 ]
完全なソースコードは以下のGitHubリポジトリで公開しています。
3-1. 純粋なCPUバウンドタスク
実行時間がほぼ完全にCPUの処理能力に依存する計算集約型のタスクです。
今回は、素数カウント(prime_count.py)を採用しました。このタスクでは、素数を探索する数値の範囲を複数のチャンクに分割し、各ワーカーが担当する範囲内の素数をカウントします。is_prime関数で素数判定を行い、count_primes_in_range関数でその個数を集計します。最後に全ワーカーの計算結果を合計することで、全体の素数の個数が得られます。
def is_prime(n: int) -> bool: """Checks if a number is prime.""" if n <= 1: return False if n == 2: return True if n % 2 == 0: return False for i in range(3, int(math.sqrt(n)) + 1, 2): if n % i == 0: return False return True def count_primes_in_range(start: int, end: int) -> int: """Counts the number of primes within a given range.""" return sum(1 for n in range(start, end) if is_prime(n))
以下のグラフは、1,000万未満の素数をカウントした時の並列数と実行時間の関係を表しています。各点は5回測定した平均値として示しています。


GILビルド(実線)の場合、threadingはワーカー数を増加させても性能がほとんど向上せず、むしろSupermicroではオーバーヘッドで性能が低下してしまいました。純粋なPythonコードによるCPUバウンドな処理はGILの影響を非常に強く受けることが確認できました。一方、GILの影響を受けないmultiprocessingは、ワーカー数を増加させることで実行時間がスケールして短縮されました。
Free-threadedビルド(点線)の場合、GILが無効化されたthreadingでワーカー数の増加に伴って性能が向上しています。MacBookにおけるthreadingとmultiprocessingの性能差は、ワーカー数が少ないうちは小さいですが、コア数に近づくにつれて大きくなっていきます。Supermicroでは、threadingとmultiprocessingとの性能差は非常に小さくなっています。また、Free-threadedビルドのmultiprocessingの性能はGILビルドよりも僅かに低下しており、2章で言及したスレッドセーフのためのオーバーヘッドが存在していると考えました。
この結果は、これまでmultiprocessingがほぼ唯一の選択肢であった純粋なCPUバウンドタスクの並列処理において、Free-threaded Pythonのthreadingが新たな選択肢となり得ることを示しています。
3-2. データ共有を伴うCPUバウンドタスク
CPU負荷が高く、複数のワーカーが巨大な共有データに対して演算を行うタスクです。
今回は配列要素の合計(array_sum.py)を採用しました。このタスクは、メモリ上に確保された巨大なNumpy配列を複数のチャンクに分割し、各ワーカーが担当する範囲の配列要素を合計します。各ワーカーはsum_array_chunk関数(共有メモリ利用時はsum_array_shm関数)を使用して、敢えて単純なforループで要素を加算していくことでCPU負荷を高くしています。最後に全ワーカーの計算結果を合計することで、全配列要素の合計を求めます。
def sum_array_chunk(data: npt.NDArray[np.int64]) -> np.int64: """Calculates the sum of a given NumPy array chunk.""" total = np.int64(0) # NOTE: Intentionally not using np.sum(data) to simulate a CPU-bound task. for value in data.reshape(-1): total += value return total def sum_array_shm( shm_name: str, shape: tuple[int], dtype: np.dtype, start: int, end: int ) -> np.int64: """Calculates the sum of a segment of a NumPy array in shared memory.""" shm = None try: shm = shared_memory.SharedMemory(name=shm_name) data: npt.NDArray[np.int64] = np.ndarray(shape, dtype=dtype, buffer=shm.buf)[ start:end ] # NOTE: Intentionally not using np.sum(data) to simulate a CPU-bound task. total = np.int64(0) for value in data.reshape(-1): total += value finally: if shm: shm.close() return total
以下のグラフは、長さ1億の配列要素を合計した時の並列数と実行時間の関係を表しています。各点は5回測定した平均値として示しています。


GILビルド(実線)の場合、threadingはワーカー数を増加させても性能が向上せず、ほぼ一定の結果となりました。これは、配列の合計処理を(np.sum()ではなく)GILを解放しないforループで実装したため、GILの影響を強く受けた結果だと考えられます。一方、multiprocessingはワーカー数の増加に伴って実行時間が短縮されました。また、特にMacBookではワーカー数が多い場合に共有メモリがデータ共有のオーバーヘッドを削減し、より高速になる傾向が見られました。
Free-threadedビルド(点線)の場合、GILから解放されたthreadingがワーカー数の増加に伴ってスケールしました。特にSupermicroではthreadingが全手法の中で最速となり、multiprocessingはワーカー数の増加に伴うオーバーヘッドにより、性能向上が鈍化してしまいました。一方MacBookでは、共有メモリを利用するmultiprocessingが最速であるものの、threadingや通常のmultiprocessingとの性能差は比較的小さく、いずれの手法も有効にスケールしていることが分かります。
この結果は、Free-threaded Pythonにおいてデータ共有のオーバーヘッドとハードウェア特性が並列処理手法の選択に大きく影響することを示しています。threadingのシンプルさと、共有メモリを使ったmultiprocessingの効率性の間で、状況に応じた最適な選択が求められます。
4. Free-threaded Pythonのプロファイリング
第3章のベンチマークでは、Free-threaded Pythonのthreadingが特定の条件下で高い性能を発揮することを確認できました。しかし、その実行時間の数値だけでは、パフォーマンスの真のボトルネックを特定するには不十分です。
本章では、プログラムの内部動作を分析し、リソース消費が大きい箇所を特定するプロファイリングというテクニックを利用します。Grafanaの公式ドキュメント『What is profiling?』では、プロファイリングを次のように定義しています。
Profiling is a technique used in software development to measure and analyze the runtime behavior of a program. By profiling a program, developers can identify which parts of the program consume the most resources, such as CPU time, memory, or I/O operations. You can use this information to optimize the program, making it run faster or use fewer resources.
Python用の高性能なCPU・GPU・メモリプロファイラとしてScaleneがあります。ただしScaleneはGILを前提とした設計であり、Free-threadingをサポートするためには内部構造の大幅な変更が必要であるとIssueで言及されています。
また、Rustで書かれたPython用のCPUプロファイラであるpy-spyについても、PEP703に記載されているPyObject構造体の変更により、Free-threaded Pythonでは動作しないとIssueで言及されています。
ScaleneのREADMEによると、Scaleneとpy-spyはともにマルチプロセスとマルチスレッドの両方をサポートするプロファイラですが、本記事では対象外とします。
プロファイリングにはいくつかの種類がありますが、ここではCPUプロファイリングのメモリプロファイリングの2つに絞って、Free-threaded Pythonのプロファイリングに利用できるツールを紹介します。
4-1. CPUプロファイリング
第2章でも紹介した『Python Free-Threading Guide』では、Free-threaded Pythonのプロファイリングのために、低レベルな統計的プロファイラ(サンプリングプロファイラ)であるsamplyが推奨されています。
samplyは、プログラム実行中に一定間隔(デフォルトは1ms)でコールスタックを記録します。このサンプリング結果を分析することで、どの関数がコールスタック中に最も頻繁に現れたか、つまり統計的にどの関数が最も多くのCPU時間を消費したかを特定できます。
samply自体はクロスプラットフォームで動作しますが、現時点でLinux環境のみPythonのperfプロファイリングサポートを有効化できるため、CPythonの内部関数だけでなくPythonの関数レベルで詳細な情報を取得したい場合はLinux環境が必須です。
参考:Python support for the Linux perf profiler — Python 3.13.7 documentation
実際にsamplyを利用して、第3章で利用したデータ共有を伴うCPUバウンドタスク(array_sum.py)をCPUプロファイリングしてみましょう。-X perfでLinuxのperfプロファイリングサポートを有効化している点に注意してください。
# threadingに絞って最大並列数32で実行 $ samply record python -X perf -m benchmark.cases.array_sum --max-workers 32 --filter threading # 省略 Local server listening at http://127.0.0.1:3000 Press Ctrl+C to stop.
処理が完了すると、samplyは自動的にローカルサーバーを起動してブラウザに結果が表示されます。SSH経由で実行している場合は、SSHポートフォワーディングを利用するか、Ctrl+Cでローカルサーバーを停止し、生成されたprofile.json.gzを手元にダウンロードしてからFirefox Profilerにアップロードしましょう。


上図は、Free-threaded threadingでarray_sum.pyをプロファイリングした時のThreadPoolExecuという名前のワーカースレッドのFlame Graphです。
Flame Graphでは、長方形が幅が広ければ広いほど(サンプリング回数が多く)CPU時間を多く消費したことを表しています。下にあるのが呼び出し元の関数、上にある呼び出し先の関数です。
このグラフを見ると、sum_array_chunkという関数が非常に広い幅を占めていることが分かります。これは、意図的にnp.sum()を使わずにforループで要素を加算している関数であり、このベンチマークにおける主要なCPUバウンド処理です。その下を辿っていくと、concurrent/futures/thread.pyのWorkerContext.run/_WorkItem.run/_workerや、threading.py(標準ライブラリ)のThread.run/Thread._bootstrap_inner/Thread._bootstrapなど、スレッド実行に関連する呼び出しスタックが確認できます。
このように、samplyを用いることで、Free-threaded Pythonでもマルチスレッド実行されているコードのどの部分がボトルネックになっているかを正確に特定し、的を絞った最適化を行うことができます。ベンチマークの数値だけでは分からない「なぜ遅いのか」を解明するための、非常に重要なステップです。
4-2. メモリプロファイリング
Free-threaded Pythonのメモリプロファイラとしては、Memrayが最も有力な選択肢です。MemrayはPythonコードだけでなく、C/C++拡張やPythonインタプリタ内部のメモリ割り当てまで追跡でき、解析のためにさまざまな形式のレポートを生成します。
先ほどと同様に、第3章で利用したデータ共有を伴うCPUバウンドタスク(array_sum.py)をメモリプロファイリングしてみましょう。
# threadingに絞って最大並列数32で実行 $ memray run -m benchmark.cases.array_sum --max-workers 32 --filter threading Writing profile results into memray-benchmark.cases.array_sum.4187165.bin # 省略 [memray] Successfully generated profile results. You can now generate reports from the stored allocation records. Some example commands to generate reports: /root/azuma/pyconjp2025/.venv/bin/python3 -m memray flamegraph memray-benchmark.cases.array_sum.4187165.bin
バイナリファイルが出力されるので、Flame Graphを生成します。
$ memray flamegraph memray-benchmark.cases.array_sum.4187165.bin Wrote memray-flamegraph-benchmark.cases.array_sum.4187165.html
出力されたHTMLファイルをブラウザで開いてみましょう。


5. Free-threaded Pythonの活用事例
Free-threaded Pythonの活用事例として、筆者の研究分野であるFederated Learningを取り上げます。
5-1. Federated Learningの概要

Federated Learning(連合学習、FL)とは、複数のクライアントがデータを集約することなく、協調して機械学習モデルを訓練するテクニックのことです。クライアントが機密性の高いデータを送信する代わりに、そのデータで訓練したローカルモデルのパラメータを定期的に送信することで、サーバーがそれらを集約してグローバルモデルを構築できるため、プライバシー保護の観点から注目を集めています。
FLについてより詳しく知りたい方は、以下をご参照ください。
初めて「Federated Learning」という用語を定義し、FLアルゴリズムを提案したのが以下の論文です。
この論文で提案されたFedAvgというアルゴリズムは、最も基本的かつ広く利用されているFLの手法です。具体的には、各クライアントがローカルデータでモデルを訓練し、そのモデルパラメータをサーバーに送信します。サーバーは受信したモデルパラメータの重み付き平均をとって新しいグローバルモデルを更新し、再びクライアントに配布します。これを繰り返すことで、クライアントがデータを直接共有することなく、全体として高性能なグローバルモデルを獲得することができます。
FedAvgをきっかけに、通信コストの削減やデータ・システムの異質性への対応、敵対的攻撃への耐性強化など、様々な課題に取り組むFLアルゴリズムが数多く提案されています。
5-2. FLフレームワークと並列処理
現実世界でFLアルゴリズムの検証を行う場合、大量の物理マシンを調達・設定・管理する必要があり非常にコストがかかります。そこで有効なのがシミュレーションです。FLフレームワークを用いることで、数百から数千のクライアントを模擬しながら効率的にFLアルゴリズムを検証することができます。また、データ・システムの異質性やクライアントの参加・離脱といった、より現実的なシナリオを柔軟に再現することも可能です。
FLシミュレーションでは、サーバーと多数のクライアントが反復的に通信し、各ラウンドでサイズの大きいモデルパラメータを交換します。クライアント処理を直列で実行することもできますが、効率的な実験サイクルを回すためには並列化が不可欠です。
代表的なFLフレームワークは、それぞれ異なるアプローチで並列化を実現しています。
Flower:汎用的な分散処理フレームワークであるRayをバックエンドに利用します。シミュレーション対象のクライアント(オブジェクト)を、利用可能なリソースに応じて作成したRay Actor(ワーカープロセス)のプールにタスクとして投入することで並列実行します。シングルノードからマルチノードクラスタまでシームレスにスケールするのが特徴です。
NVFlare:組み込みのFL Simulator を備えており、サーバと複数クライアントを単一プロセス内のマルチスレッドでシミュレーションします。この軽量なアプローチにより、シングルノードでの迅速な検証が可能です。クライアント群を指定したGPUに分配し、別々のプロセスとして実行することもできます。
FedML:大規模分散学習を想定して設計されており、MPI(Message Passing Interface)や NCCL(NVIDIA Collective Communications Library)といった通信バックエンドを利用して複数GPU環境における効率的な分散処理を実現します。特に、高速なGPU間通信(例: NVLink, InfiniBand)が利用可能な環境では NCCLベースのMPIシミュレーションが推奨されます。
pfl-research:研究を加速させるためのシミュレーションフレームワークであり、分散学習ライブラリであるHorovodを主なバックエンドとして利用し(
tf.distributeやtorch.distributedもサポート)、単一プロセスでの実行から、マルチプロセス、マルチGPU、マルチノード環境へとシームレスにスケールアップできます。BlazeFL:シングルノードでのシミュレーション効率を重視し、Free-threaded Pythonを活用したマルチスレッドモードを中核に据えています。
threadingを用いた真の並列処理と、メモリ空間の共有による低オーバーヘッドなデータ共有を実現します。torch.multiprocessingを活用したマルチプロセスモードもサポートしています。
本記事ではRayベースのFlowerと、筆者が開発しているBlazeFLに焦点を当てて、そのアーキテクチャと利点を掘り下げます。
5-2-1. Flower
Flowerはカスタマイズ性が高く、PyTorchやTensorFlow、JAXなど多くの機械学習フレーワークに対応している著名なFLフレームワークです。Flowerのシミュレーションエンジンは、Rayをベースに構築されています。
RayはスケーラブルなMLワークロードのための分散処理フレームワークであり、シングルノードからマルチノードまで同じPythonコードでスケールします。ステートレスなタスク(関数)は@ray.remoteというデコレーターを追加するだけ分散実行させることができます。クラスについてはActorというステートフルなワーカーをインスタンス化すると、そのActorのメソッドのスケジュール先となるワーカープロセスが作成されます。
RayベースのFlowerシミュレーションエンジンでは、複数のワーカー(Pythonプロセス)を含むRayBackend(デフォルトのバックエンド)がClientAppというオブジェクトを生成・管理します。実行するClientAppがワーカー数よりも多い場合は、リソースが解放されてから実行されるため並列数をコントロールすることができます。例えば、1ラウンドで100クライアントを処理する設定で、システムリソースの都合上10クライアントまでしか並列化できない場合、100個のClientAppは10個ずつのバッチで順次実行されます。
Rayは、各ノードに存在するオブジェクトストアにオブジェクトを保存します。複数のオブジェクトストアを組み合わせることで、クラスタ全体で分散共有メモリのような仕組みを提供します。オブジェクトは不変(immutable)であるため、オブジェクトが複数のノードに複製されてもデータを同期させる必要がなく、高いパフォーマンスを実現しています。あるノードで参照先のオブジェクトがオブジェクトストアに存在しない場合は、そのオブジェクトを持つ別のノードから転送され、ローカルストアにキャッシュされます。
オブジェクトがNumpy配列やPandasデータフレームなど、Apache Arrow形式と互換性がある場合は、プロセスは共有メモリ上のデータを直接参照するゼロコピーでの読み取りが可能で非常に高速です。一方、それ以外のPythonオブジェクトの場合、共有メモリからPythonオブジェクトを復元する必要があるため、データコピーとデシリアライズという2つのコストがかかります。
このインメモリオブジェクトストアであるPlasmaは、もともとRayプロジェクトの一部として開発されました。その後、より広範なプロジェクトで利用できるようにApache Arrowプロジェクトに移管されましたが、Rayは独自の最適化を行うためPlasmaをフォークして開発を継続しています。一方、Apache ArrowではPlasmaの開発は終了し、既に削除されています。
5-2-2. BlazeFL
BlazeFLは、あえてシングルノードでの高速なシミュレーションに特化して「シンプルさ」と「高速性」という2つの価値を追求するために設計されたFLフレームワークです。
このシンプルさは、FL研究の現実的なユースケースに焦点を当てることから生まれています。多くの論文で扱われるモデルはコンシューマー向けGPUを搭載した単一マシンで十分に訓練可能であり、実験は再現性の観点からコンテナ上で行われるのが一般的です。BlazeFLはまさにこの環境をターゲットとし、マルチノード化に伴うKubernetesのようなコンテナオーケストレーションツールの複雑さを意図的に回避しています。
この思想は、PyTorch以外に主要な依存ライブラリを持たないというミニマルな設計にも現れています。標準ライブラリ中心の軽量なコードベースは、フレームワーク全体を透明でカスタマイズ可能な状態に保ちます。これにより、研究者は複雑な抽象化に悩まされることなく、コアロジックを容易に理解し、自身の研究に合わせて自由にカスタマイズすることが可能です。
そして、もう一つの価値である高速性を追求するため、BlazeFLは特性の異なる2つの並列実行モードを提供します。
マルチプロセスモード: 安定した従来型の並列化手法です。
multiprocessingのラッパーでPyTorchのテンソル共有に特化したtorch.multiprocessingを活用し、サイズの大きいモデルパラメータ(torch.Tensor)を共有メモリに配置します。これにより、各ワーカプロセスはゼロコピーでテンソルを参照でき、デシリアライズのコストを削減します。ただし、プロセス自体の生成コストは避けられません。マルチスレッドモード(Python 3.13ではExperimental): 本記事のテーマであるFree-threaded Pythonを活用する先進的なモードです。GILが無効化された環境でクライアントをスレッドとして実行することで、真の並列処理を実現します。スレッドはメモリ空間を共有するため、プロセス間通信で発生するシリアライズやデータコピーのオーバーヘッドがほぼゼロになります。これにより、
multiprocessingのような共有メモリ管理の複雑さから解放され、研究者はほぼ通常のPyTorchコードを書く感覚で、効率的なFLシミュレーションを実行できます。
5-3. FlowerとBlazeFLのベンチマーク
理論上の利点だけでなく、実際のワークロードでどの程度の性能差が生まれるかを検証するため、FlowerとBlazeFLのFLシミュレーション性能を比較するベンチマークを実施します。
ベンチマークはバックグラウンドプロセスは最小限に抑えた、以下のシングルノード・マルチGPU環境で実施しました。
- TYAN S8030GM2NE (OS: Ubuntu 24.04.2 LTS, Chip: AMD EPYC 7542, Cores: 32, Memory: 256 GB, GPU: 4 × Quadro RTX 6000)
今回のベンチマークでは、執筆時点の最新の安定版であるPython 3.13を使用しました。これは、各種ライブラリの対応が過渡期にあるためです。具体的には、両フレームワークが共通して依存するtorchがまだPython 3.14向けの公式wheelを提供しておらず、Flowerが依存するcffiはPython 3.14からしかFree-threadedビルドに対応していません。
これらの制約から各フレームワークが動作する最新の環境として、Flowerは3.13.7、BlazeFLは3.13.7 experimental free-threading buildを選択しました。第2章で言及したように、Free-threadedビルドはGILありのビルドと比較してシングルスレッド性能が低下するため、今回のベンチマークではBlazeFL側に僅かながら不利な条件が含まれている点に注意してください。
FLシナリオとしては、一般的なFL研究を想定し、両フレームワークで条件を統一しました。具体的には、アルゴリズムはFedAvg、データセットはCIFAR-10、クライアント数は100、ラウンド数は5、モデルは量なCNNと中規模のResNet-18を採用し、バッチサイズなどのハイパーパラメータは全て統一しています。なお、PyTorchのDataLoaderは画像の前処理でCPUを利用するため、このタスクはCPUとGPUの両方を使用する混合バウンドなワークロードとなります。
Rayの利用可能リソース確認
FlowerのバックエンドであるRayは、モデルパラメータなどのオブジェクトをインメモリのオブジェクトストアで管理します。このストアの容量が不足すると、オブジェクトのスピル(ディスクへの退避)が発生し、深刻なパフォーマンス低下を引き起こす可能性があります。そのため、実験前にray.available_resources()を用いて、リソースが十分であることを確認しました。
$ uv run python Python 3.13.7 (main, Sep 2 2025, 14:21:46) [Clang 20.1.4 ] on linux Type "help", "copyright", "credits" or "license" for more information. >>> import ray >>> ray.init() 2025-09-20 17:29:40,090 INFO worker.py:1951 -- Started a local Ray instance. 2025-09-20 17:29:40,101 INFO packaging.py:588 -- Creating a file package for local module '/home/azuma/benchmarks/flower-case'. 2025-09-20 17:29:40,109 INFO packaging.py:380 -- Pushing file package 'gcs://_ray_pkg_e15f9b005451107a.zip' (0.30MiB) to Ray cluster... 2025-09-20 17:29:40,113 INFO packaging.py:393 -- Successfully pushed file package 'gcs://_ray_pkg_e15f9b005451107a.zip'. RayContext(dashboard_url='', python_version='3.13.7', ray_version='2.49.1', ray_commit='c057f1ea836f3e93f110e895029caa32136fc156') >>> resources = ray.available_resources() >>> print(resources) {'node:192.168.1.220': 1.0, 'node:__internal_head__': 1.0, 'CPU': 64.0, 'memory': 177717184512.0, 'accelerator_type:RTX': 1.0, 'object_store_memory': 76164507648.0, 'GPU': 4.0} >>> exit()
上記の通り、オブジェクトストアのメモリ(object_store_memory)は約70GB確保できています。今回最大のモデルであるResNet-18を100クライアント分ロードした場合の総サイズ(約11.7Mパラメータ × 4バイト/パラメータ × 100クライアント ≈ 4.5GB)を大きく上回っており、オブジェクトストアがボトルネックにならないことを確認しています。
完全なソースコードは以下のGitHubリポジトリで公開しています。
以下に示すグラフは、CNNモデルとResNet-18モデルを使用し、並列ワーカー数を変更しながらシミュレーション全体の実行時間を5回計測した平均値です。エラーバーは標準偏差を表します。


CNNとResNet-18の両方のモデルにおいて、BlazeFLのマルチスレッドモードがほぼ全ての並列数で最速の結果を示しました。これは、Free-threaded Pythonの恩恵を最大限に活用できていることを示唆しています。スレッド間でメモリ空間を共有するため、各ラウンドで交換されるモデルパラメータのシリアライズ/デシリアライズやデータコピーのオーバーヘッドがほとんど発生しません。GILが無効化されているため、CPU+GPUバウンドなクライアントのモデル訓練を真に並列化でき、性能が大幅に向上しています。
BlazeFLのマルチスレッドモードは、ワーカー数が16あたりまでは良好にスケールしますが、それを超えると性能が頭打ち、あるいは低下する傾向が見られます。これは、CPUコア数(32コア)に近づくにつれて、コンテキストスイッチのオーバーヘッドやリソース競合が無視できなくなったためと考えられます。
BlazeFLのマルチプロセスモードでは、並列化によって性能は向上しますが、マルチスレッドモードほどのスケーラビリティは見られません。特にCNNモデルでは8ワーカーを超えると性能が著しく低下しており、クライアント数に等しいプロセスの生成コストと、共有メモリを使用しているとはいえシリアライズにかかるオーバーヘッドが大きいためと推測されます。
FlowerはRayをバックエンドとして利用しておりスケールしますが、BlazeFLと比べて特に並列数が1の時点で実行時間が長くなっています。これは、Rayが汎用的な分散処理フレームワークであり、オブジェクトストアの管理やスケジューリングなどが、シングルノードのシミュレーションに比べては、オーバーヘッドが大きくなるためだと考えられます。
モデルサイズの影響として、軽量なCNNモデルと比べてより計算負荷の高いResNet-18の方が並列化による恩恵を受けやすくなっています。一方、BlazeFLのマルチスレッドモードと他の手法との性能差はCNNでより顕著に現れています。これは、モデルの計算時間に対する通信やデータ共有のオーバーヘッドの割合が相対的に大きくなるため、オーバーヘッドを削減することで純粋な並列計算能力に近い速度が出るためだと考えられます。
あくまでもシングルノードにおけるFLシミュレーションという設定ですが、Free-threaded Pythonを活用したBlazeFLのマルチスレッドモードが他の手法を圧倒する高いパフォーマンスとスケーラビリティを発揮しました。これは、特にデータ共有が頻繁に発生するCPUバウンドなタスクにおいてthreadingがmultiprocessingやRayのような分散フレームワークを上回る有力な選択肢であると言えます。
6. threadingの落とし穴
Free-threaded Pythonの登場により、threadingはCPUバウンドなタスクでも真の並列処理を実現できるようになりました。しかし、threadingを安全に使いこなすには、複数のスレッドがメモリ空間を共有することによって生じる課題と向き合う必要があります。特に、競合状態はFree-threadingによって顕在化し、デッドロックはthreadingによってより身近な問題となります。
本章では、なぜこれらの問題が起きるのか、そしてどうすれば安全にスレッドを扱えるのかを解説します。
6-1. 競合状態(Race Condition)
競合状態とは、2つ以上のスレッドが共有データに同時にアクセスし、処理が実行される順序によって最終的な結果が変わってしまう、意図しない状況を指します。
まずは、複数のスレッドがデータを同時に書き換えようとして発生する競合状態の例を見てみましょう。以下のコードは、2つのスレッドがそれぞれ10万回ずつ、グローバル変数counterをインクリメントする単純なプログラムです。最終的にcounterは20万になることを期待しています。
import threading counter = 0 def worker(iterations: int) -> None: global counter for _ in range(iterations): counter += 1 if __name__ == "__main__": ITERATIONS_PER_THREAD = 100_000 t1 = threading.Thread(target=worker, args=(ITERATIONS_PER_THREAD,)) t2 = threading.Thread(target=worker, args=(ITERATIONS_PER_THREAD,)) t1.start() t2.start() t1.join() t2.join() expected = 2 * ITERATIONS_PER_THREAD print(f"Expected: {expected}, Actual: {counter}")
このコードを、GILありビルドとFree-threadedビルドでそれぞれ実行してみると、全く異なる結果が得られます。
# GILありビルド $ uv run python -VV Python 3.14.0rc3 (main, Sep 18 2025, 19:55:15) [Clang 20.1.4 ] $ for i in {1..10}; do uv run python race_condition.py; done Expected: 200000, Actual: 200000 Expected: 200000, Actual: 200000 Expected: 200000, Actual: 200000 Expected: 200000, Actual: 200000 Expected: 200000, Actual: 200000 Expected: 200000, Actual: 200000 Expected: 200000, Actual: 200000 Expected: 200000, Actual: 200000 Expected: 200000, Actual: 200000 Expected: 200000, Actual: 200000
GILありビルドの場合、何度実行しても期待通り20万という結果になります。
# Free-threadedビルド $ uv run python -VV Python 3.14.0rc3 free-threading build (main, Sep 18 2025, 19:37:18) [Clang 20.1.4 ] $ for i in {1..10}; do uv run python main.py; done Expected: 200000, Actual: 102344 Expected: 200000, Actual: 105768 Expected: 200000, Actual: 102739 Expected: 200000, Actual: 101825 Expected: 200000, Actual: 106348 Expected: 200000, Actual: 102701 Expected: 200000, Actual: 101180 Expected: 200000, Actual: 101508 Expected: 200000, Actual: 100588 Expected: 200000, Actual: 103120
一方、Free-threadedビルドの場合は実行するたびに結果が変わり、期待した値になりません。
この現象の鍵は、counter += 1という処理がアトミック(不可分)な操作ではないことにあります。この1行のコードは、内部的に少なくとも以下の3ステップのバイトコード命令に分解されて実行されます。
- 読み出し:メモリから
counterの現在の値を読み出す - 変更:読み込んだ値に1を加算する
- 書き戻し:計算結果を
counterに書き戻す
Free-threadedビルドでは、複数のスレッドが文字通り同時に別々のCPUコアで実行されるため、この3ステップの途中でスレッドの実行が割り込まれる可能性が非常に高くなります。その結果、以下のような更新の消失(Lost Update)が頻繁に発生します。
- スレッドAが
counterの値100を読み出す - ほぼ同時に、スレッドBも
counterの値100を読み出す - スレッドAが
100 + 1を計算し、結果の101をcounterに書き戻す - スレッドBも
100 + 1を計算し、結果の101をcounterに書き戻す(スレッドAの更新を上書き)
このシナリオでは、2回インクリメントしたにもかかわらず、counterの値は1しか増えていません。これが、最終的な結果が期待していた値より遥かに小さくなる原因です。
では、なぜGILありビルドでは問題が起きなかったのでしょうか。GILは、一度に1つのスレッドしかPythonバイトコードを実行できないようにする仕組みです。counter += 1に対応する一連のバイトコードは非常に短いため、CPythonのスケジューラがスレッドを切り替える(デフォルトで5ミリ秒ごと)前に読み出しから書き戻しまでの一連の処理が完了してしまうことがほとんどです。これにより、競合状態が発生する可能性が極めて低くなり、問題が表面化していなかったのです。
import dis counter = 0 def f() -> None: global counter counter += 1 if __name__ == "__main__": dis.dis(f)
$ python disassemble.py 5 RESUME 0 7 LOAD_GLOBAL 0 (counter) LOAD_SMALL_INT 1 BINARY_OP 13 (+=) STORE_GLOBAL 0 (counter) LOAD_CONST 1 (None) RETURN_VALUE
LOAD_GLOBAL(読み出し)、BINARY_OP(変更)、STORE_GLOBAL(書き戻し)という複数の命令に分かれていることが明確に分かります。
この問題を解決するには、counterへのアクセスを一度に1つのスレッドに限定する排他制御が必要です。これにはthreading.Lockを使います。
+ lock = threading.Lock() def worker(iterations: int) -> None: global counter for _ in range(iterations): - counter += 1 + with lock: + counter += 1
with lock:で囲まれたコードブロックは、あるスレッドがこのブロックを実行している間、他のスレッドはブロックの入り口で待機させられます。これにより、読み出し・変更・書き戻しの一連の処理が中断されることなく安全に実行され、Free-threadedビルドでも常に期待通りの結果が得られます。
6-2. デッドロック(Deadlock)
競合状態と並んで、マルチスレッドプログラミングで注意すべきもう一つの典型的な問題がデッドロックです。デッドロックとは、2つ以上のスレッドが互いに相手が保持しているリソース(この場合はロック)の解放を待ち続けて、永遠に処理が進まなくなる状態を指します。
以下のコードは、2つのスレッドが2つのロックaとbを、それぞれ異なる順序で取得しようとするプログラムです。
import threading import time a = threading.Lock() b = threading.Lock() def worker1() -> None: with a: print("thread 1 got lock a") time.sleep(0.1) with b: print("thread 1 got lock b") def worker2() -> None: with b: print("thread 2 got lock b") time.sleep(0.1) with a: print("thread 2 got lock a") if __name__ == "__main__": t1 = threading.Thread(target=worker1) t2 = threading.Thread(target=worker2) t1.start() t2.start() t1.join() t2.join() print("all threads have finished")
このコードを実行すると、プログラムは途中で停止(ハング)してしまいます。
$ python deadlock.py thread 1 got lock a thread 2 got lock b
このプログラムは、以下のシナリオでデッドロックに陥ります。
- スレッド1がロック
aを取得する - ほぼ同時に、スレッド2がロック
bを取得する - スレッド1は次にロック
bを取得しようとするが、スレッド2によって保持されているため待機状態になる - スレッド2は次にロック
aを取得しようとするが、スレッド1によって保持されているため待機状態になる
この結果、スレッド1はスレッド2を、スレッド2はスレッド1を永遠に待ち続ける循環待ち(Circular Wait)状態になり、プログラムが停止してしまうのです。
競合状態とは異なり、デッドロックはGILの有無に直接関係なく発生し得る、マルチスレッドにおける普遍的な問題です。しかし、Free-threaded Pythonによってthreadingの利用範囲がCPUバウンドなタスクにも広がることで、これまで以上に身近な課題となります。
デッドロックを回避するための最も確実で一般的な方法は、複数のロックを取得する際の順序を全てのスレッドで統一することです。これにより、循環待ちの発生を防ぐことができます。
以下の例では、worker2もworker1と同じく、必ずロックa→bの順で取得するように修正します。
def worker2() -> None: - with b: - print("thread 2 got lock b") + with a: + print("thread 2 got lock a") time.sleep(0.1) - with a: - print("thread 2 got lock a") + with b: + print("thread 2 got lock b")
$ python deadlock.py thread 1 got lock a thread 1 got lock b thread 2 got lock a thread 2 got lock b all threads have finished
全てのスレッドが同じ順序でロックを取得しようとするため、どちらかのスレッドが先に両方のロックを獲得し、もう一方が待機します。処理が終わればロックが解放され、待っていたスレッドが処理を再開できるため、デッドロックは発生しません。
ロックの取得順序を厳密に管理するのが難しい複雑なケースでは、ロック取得処理にタイムアウトを設ける方法も有効です。Lock.acquire()メソッドは、ブロッキングする場合にtimeout引数を指定できます。
def worker2() -> None: - with b: - print("thread 2 got lock b") - time.sleep(0.1) - with a: - print("thread 2 got lock a") + while True: + if b.acquire(): + print("thread 2 got lock b") + time.sleep(0.1) + if a.acquire(timeout=1): + print("thread 2 got lock a") + a.release() + b.release() + break + else: + print("thread 2 failed to get lock a, retrying...") + b.release() + time.sleep(0.1)
thread 1 got lock a thread 2 got lock b thread 2 failed to get lock a, retrying... thread 1 got lock b thread 2 got lock b thread 2 got lock a all threads have finished
このコードでは、worker2はロックaの取得を1秒だけ試みます。もし取得できなければacquire()はFalseを返すため、一度保持しているロックbを解放し、少し待ってから再試行します。このようにリソースを一時的に手放すことで、もう一方のスレッドに処理を進める機会を与え、デッドロックを能動的に解消することができます。
まとめ
本記事では、Python 3.14 で正式にサポートされたFree-threaded Pythonについて、並行処理と並列処理の基礎からmultiprocessingとの性能比較、プロファイリング手法、Federated Learningへの応用事例、そしてthreading特有の注意点に至るまで、網羅的に解説しました。
Free-threaded Pythonの登場は、「CPUバウンドタスクにはmultiprocessing一択」という長年の常識を覆し、軽量性と真の並列性を両立したthreadingをシンプルかつ強力な選択肢として確立しました。
この変更は、機械学習やデータ分析といった分野だけでなく、あらゆるPythonistaがマルチコアCPUの恩恵を容易に受けられるようになることを意味します。まだオプション機能ではありますが、将来的なデフォルト化も見据えられている今、この新しい並列処理を探求することは全ての開発者にとって大きな価値があると考えています。
ぜひ本記事で紹介したコードを試しながら、自分のユースケースにどう活かせるかを探ってみてください。
