この記事は「渡部 Advent Calendar 2025」の4日目の記事です。
ざっくりいうとCPU の「コア数」を使って、複数のスレッドが同時に物理的に実行されている状態。
- 「並列(parallel)」=物理的に同時
- 「並行(concurrent)」=論理的に同時(実際は切り替え)
並列性とは具体的に何を指す?
複数の CPU コアが存在し、それぞれが同時に処理を実行している
たとえば 4 コア CPU だと
Core0: スレッドA を実行 Core1: スレッドB を実行 Core2: スレッドC を実行 Core3: スレッドD を実行
これは 完全な並列実行。4 つのタスクが本当に同時に動いている。
GIL が存在しない言語では、CPU-bound 処理がスレッドで高速化できる
- C, C++, Go, Java, Rust → どれもスレッドで普通に並列化可能
- Python でも C拡張が重めの処理(NumPy, OpenCV など)は GIL を release して並列性を獲得できる。
Python は GIL で基本「並列性」がない
Python(CPython)は GIL によ同時に動ける Python bytecode は 1 つだけ。よって Python 純粋コードは「並列」は不可能で「並行」止まり
Thread-1: |----実行----| Thread-2: |----実行----|
実際は CPU が高速に切り替えているだけ(並行・concurrency)。
GILの実装
GIL は「Python バイトコード実行を 1 スレッドに制限するためのミューテックス」。内部的には以下で構成されています
- POSIX threads(pthread)や OS ネイティブの mutex
- 条件変数(Condition Variable)による“GIL の譲渡”メカニズム
- 「チェック間隔 / タイムスライス」によるスレッド切り替えの仕組み
- GIL の保持・解放を自動管理するマクロ(PyGILState_Ensure など)
GIL の状態は _gil_runtime_state という構造体で管理されている。
typedef struct { PyMutex mutex; // GIL 本体(排他ロック) PyCondVar cond; // スレッド切り替え用の条件変数 int locked; // ロックされているか? PyThreadState *owner; // GIL を保持しているスレッド _Py_atomic_address last_holder; // デバッグ用 int switch_number; // スレッド切り替え回数 } _gil_runtime_state;
スレッドはどうやって GIL を取っているのか?
Python のスレッド(PyThreadState)はバイトコードを実行する前に 必ず GIL の取得処理を行う。
内部コードでは次のように mutex を lock する。
PyMutex_Lock(&gil.mutex); gil.locked = 1; gil.owner = PyThreadState_GET();
これでそのスレッドは Python バイトコードを実行できる。
GILの解放
自動解放メカニズム
In order to emulate concurrency of execution, the interpreter regularly tries to switch threads (see :func:`sys.setswitchinterval`). The lock is also released around potentially blocking I/O operations like reading or writing a file, so that other Python threads can run in the meantime.
ブロッキングI/O操作時
GILはファイル読み書きなどのブロッキングI/O操作の前後に自動的に解放・再取得されます init.rst:784-786 。これによりI/O待機中に他のスレッドがPythonコードを実行できます。以下はIO操作時のスレッドの解放処理です。
static int _enter_buffered_busy(buffered *self) { int relax_locking; PyLockStatus st; if (self->owner == PyThread_get_thread_ident()) { PyErr_Format(PyExc_RuntimeError, "reentrant call inside %R", self); return 0; } PyInterpreterState *interp = _PyInterpreterState_GET(); relax_locking = _Py_IsInterpreterFinalizing(interp); Py_BEGIN_ALLOW_THREADS if (!relax_locking) st = PyThread_acquire_lock(self->lock, 1); else { /* When finalizing, we don't want a deadlock to happen with daemon * threads abruptly shut down while they owned the lock. * Therefore, only wait for a grace period (1 s.). * Note that non-daemon threads have already exited here, so this * shouldn't affect carefully written threaded I/O code. */ st = PyThread_acquire_lock_timed(self->lock, (PY_TIMEOUT_T)1e6, 0); } Py_END_ALLOW_THREADS if (relax_locking && st != PY_LOCK_ACQUIRED) { PyObject *ascii = PyObject_ASCII((PyObject*)self); _Py_FatalErrorFormat(__func__, "could not acquire lock for %s at interpreter " "shutdown, possibly due to daemon threads", ascii ? PyUnicode_AsUTF8(ascii) : "<ascii(self) failed>"); } return 1; }
タイムスライスによる強制スイッチ
sys.setswitchinterval()で設定された間隔(デフォルト5ms)で強制的にスレッド切り替えが発生し、GILが解放されます
手動解放メカニズム
C 拡張で GIL を解放する処理
// example.c #define PY_SSIZE_T_CLEAN #include <Python.h> #include <unistd.h> // sleep() // Python 側から呼び出される関数: sleep(seconds) するだけ static PyObject* my_sleep(PyObject* self, PyObject* args) { int seconds; if (!PyArg_ParseTuple(args, "i", &seconds)) { return NULL; } // ここから GIL を解放 Py_BEGIN_ALLOW_THREADS // ---- ここは GIL なしで実行される領域 ---- // ・他スレッドの Python コードと真の並列実行が可能 // ・ただし Python オブジェクトへのアクセスは禁止 sleep(seconds); // ------------------------------------- // ここで GIL を再取得 Py_END_ALLOW_THREADS Py_RETURN_NONE; } // モジュール定義 static PyMethodDef ExampleMethods[] = { {"my_sleep", my_sleep, METH_VARARGS, "Sleep without holding the GIL"}, {NULL, NULL, 0, NULL} }; static struct PyModuleDef examplemodule = { PyModuleDef_HEAD_INIT, "example", NULL, -1, ExampleMethods }; PyMODINIT_FUNC PyInit_example(void) { return PyModule_Create(&examplemodule); }
OSのプリエンプションとGILのタイムスライスは異なる抽象レベルで動作する
Linux でも macOS でも Windows でも、スレッドは OS によってプリエンプティブにスケジュールされます。つまりスレッドAが CPU を使っていても、OS は強制的に A を止めて B に切り替えるこれは Python だろうと Java だろうと C だろうと同じでPython は OS のスケジューラを変更することはできないです。
相互作用の詳細
- 実行フロー
重要な違いとしてはこんな感じ。
まとめ
- 並列(parallel)=CPU の複数コアで“物理的に同時実行”。
- 並行(concurrent)=実際は1つを高速に切り替えて“同時に見えるだけ”。
Python(CPython)は GIL により1つのスレッドしか Python バイトコードを実行できないため、基本は「並行」止まりで「並列」はできない。
GIL は OS の mutex+条件変数で実装され、I/O や C拡張では 自動的に解放されるため、その間は並列になる。
OS のプリエンプション(強制切替)とGIL のスレッド切替(協調動作)はまったく別レイヤー。