10
0

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でマルチスレッドにしたのに速くならない...」

その原因は GIL(Global Interpreter Lock)

これを理解してないと、Pythonの並列処理でハマるよ。

GILとは?

GIL = グローバルインタプリタロック

Pythonインタプリタが一度に1つのスレッドしかPythonバイトコードを実行しないようにするロック。

┌───────────────────────────────────────┐
│          Python Interpreter           │
│  ┌─────────────────────────────────┐  │
│  │           GIL 🔒                │  │
│  │  ここを通れるのは1スレッドだけ  │  │
│  └─────────────────────────────────┘  │
│                                       │
│  Thread1 ──→ 待機                     │
│  Thread2 ──→ 実行中 🏃               │
│  Thread3 ──→ 待機                     │
│  Thread4 ──→ 待機                     │
└───────────────────────────────────────┘

実際に計測してみる

CPU負荷タスク

def cpu_bound_task(n: int) -> int:
    """CPU負荷の高い計算"""
    count = 0
    for i in range(n):
        count += i * i
    return count

シングルスレッド

# 4回順番に実行
cpu_bound_task(10_000_000)
cpu_bound_task(10_000_000)
cpu_bound_task(10_000_000)
cpu_bound_task(10_000_000)
# => 3.07秒

マルチスレッド

threads = []
for _ in range(4):
    t = threading.Thread(target=cpu_bound_task, args=(10_000_000,))
    threads.append(t)
    t.start()

for t in threads:
    t.join()
# => 2.93秒  ← 速くなってない!

4スレッドなのにほぼ同じ時間! これがGILの影響。


I/O負荷なら速くなる

GILはI/O待ちの間解放される

def io_bound_task(seconds: float):
    time.sleep(seconds)  # I/O待ち中はGILを解放

結果

方式 時間
シングルスレッド 2.00秒
マルチスレッド 0.50秒 ← 4倍速!

I/O待ち(ネットワーク、ファイル読み書き、sleep)では並行処理が効く!


なぜGILがあるの?

CPythonのメモリ管理

CPythonは参照カウントでメモリ管理している:

a = []      # refcount = 1
b = a       # refcount = 2
del a       # refcount = 1
del b       # refcount = 0 → 解放

マルチスレッドで参照カウントを同時に更新すると壊れる。

GILは参照カウントを守るための単純な解決策。

なぜ今も残ってる?

  • 取り除くとシングルスレッド性能が落ちる
  • C拡張との互換性が壊れる
  • 歴史的経緯(最初からあった)

GILの回避方法

1. multiprocessing(プロセス分離)

from multiprocessing import Pool

def cpu_bound(n):
    return sum(i * i for i in range(n))

with Pool(4) as pool:
    results = pool.map(cpu_bound, [10_000_000] * 4)
# プロセスごとに別のGIL → 真の並列実行

2. concurrent.futures

from concurrent.futures import ProcessPoolExecutor

with ProcessPoolExecutor(max_workers=4) as executor:
    futures = [executor.submit(cpu_bound, 10_000_000) for _ in range(4)]
    results = [f.result() for f in futures]

3. Cython / C拡張

GILを明示的に解放できる:

with nogil:
    # ここではGILなしで実行
    heavy_computation()

4. NumPy / SciPy

内部でGILを解放してる:

import numpy as np

# NumPyの演算は内部でGILを解放して並列実行
result = np.dot(large_matrix1, large_matrix2)

5. asyncio(I/O向け)

async def fetch_all():
    await asyncio.gather(
        fetch_url("https://api1.example.com"),
        fetch_url("https://api2.example.com"),
        fetch_url("https://api3.example.com"),
    )

使い分けチャート

タスクの種類は?
    │
    ├─ CPU負荷(計算、画像処理など)
    │      │
    │      └─ multiprocessing / ProcessPoolExecutor
    │
    └─ I/O負荷(ネットワーク、ファイルなど)
           │
           ├─ 同期的 → threading / ThreadPoolExecutor
           │
           └─ 非同期 → asyncio

Python 3.13+ の変化

Free-threaded Python(実験的)

Python 3.13からGILなしのビルドが実験的に利用可能:

# GILなしPythonのインストール(将来)
python3.13t  # t = thread-safe

まだ実験段階だが、将来的にはGILが消えるかも?


まとめ

状況 解決策
CPU負荷の並列化 multiprocessing
I/O負荷の並行化 threading or asyncio
数値計算 NumPy(内部でGIL解放)
最大パフォーマンス Cython, C拡張

覚えること

  1. GIL = Pythonは1スレッドしか同時実行できない
  2. CPU負荷 → スレッドでは速くならない
  3. I/O負荷 → スレッドでも速くなる
  4. CPU並列化したいなら multiprocessing

これでGIL完全理解!🎄

10
0
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
10
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?