この記事は「渡部 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;
PyCondVar cond;
int locked;
PyThreadState *owner;
_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 {
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 を解放する処理
#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include <unistd.h>
static PyObject* my_sleep(PyObject* self, PyObject* args)
{
int seconds;
if (!PyArg_ParseTuple(args, "i", &seconds)) {
return NULL;
}
Py_BEGIN_ALLOW_THREADS
sleep(seconds);
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 のスケジューラを変更することはできないです。
相互作用の詳細
- 実行フロー
- OSがPythonスレッドをCPUにスケジュール
- スレッドがGILを取得してPythonバイトコードを実行
- OSがスレッドをプリエンプトしても、GILは保持されたまま
- スレッドが再開されると、引き続きGILを持って実行を継続
- setswitchintervalの時間が経過すると、GILが解放されて他のPythonスレッドに譲渡
重要な違いとしてはこんな感じ。
- OSプリエンプション: いつでも発生、強制的、ハードウェアレベル
- GILスイッチ: バイトコード命令間のみ、協調的、インタープリタレベル
まとめ
- 並列(parallel)=CPU の複数コアで“物理的に同時実行”。
- 並行(concurrent)=実際は1つを高速に切り替えて“同時に見えるだけ”。
Python(CPython)は GIL により1つのスレッドしか Python バイトコードを実行できないため、基本は「並行」止まりで「並列」はできない。
GIL は OS の mutex+条件変数で実装され、I/O や C拡張では 自動的に解放されるため、その間は並列になる。
OS のプリエンプション(強制切替)とGIL のスレッド切替(協調動作)はまったく別レイヤー。