PyCon JP 2024 Sprint で Python free threading (No GIL)対応を検証するPytest Plugin開発プロジェクトに参加してきました

[PyCon JP 2024の開発Sprint](https://pyconjp.connpass.com/event/329084/)にて、[pytest-freethreaded](https://github.com/tonybaloney/pytest-freethreaded)の開発に参加してきました。

私はこのプロジェクトでOSSライブラリの[Free Threading](https://docs.python.org/3.13/glossary.html#term-free-threading)対応の検証に携わり、いくつかの課題を発見しました。短期間ながらとても良い体験でしたので、本記事で、開発の背景と検証ツールの使い方、実際の検証の方法について共有します。

## Python 3.13におけるFree Threadingの導入

Free threadingは[PEP703](https://peps.python.org/pep-0703/)で提案され、Python 3.13からオプションで有効化できるようになった新機能です。Free threadingはGIL（Global Interpreter Lock）を無効にし、複数のスレッドが同時に実行されるようになります。Free Threadingに関する詳細は[公式ドキュメント](https://docs.python.org/3.14/howto/free-threading-python.html)で確認できます。

しかし、GILに（暗黙的に）依存して動作していたプログラムは、Free threadingによってエラーをおこす可能性があり、対応状況の検証が必要です。[py-free-threading](https://py-free-threading.github.io/)ではScientific Pythonを中心に[OSSライブラリのFree threadingの対応状況](https://py-free-threading.github.io/tracking/)がトラッキングされています。

この記事では、Free threadingを有効にしたPython処理系のインストール方法、Free threadingの検証のためのpytestプラグインであるpytest-freethreadedのインストール方法と使い方、実際のOSSライブラリでの検証例を紹介します。

## Free Threadingを有効にしたPython処理系のインストール手順

前述の [py-free-threadingにインストール方法](https://py-free-threading.github.io/installing_cpython/) で各OSごとのインストール方法が説明されています。

私はmacOSを使っているため、ここではmacOSでのインストール方法を紹介します。GUI、CUIどちらでもインストールできますので、順に説明します。


### 方法１：GUIインストーラ

https://www.python.org/ からPython 3.13のmacOS用のインストーラをダウンロードします。執筆時点の 2024/10/01時点では、[3.13.0rc3](https://www.python.org/downloads/release/python-3130rc3/)が最新です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/133233/561815ce-0bd0-176c-60de-e43a82d40ea1.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/133233/e0ad9b85-e4c6-d5c7-61bb-f9aa353574e5.png)

ダウンロードしたインストーラ python-3.13.0rc3-macos11.pkg を実行し、下記のダイアログの「カスタマイズ」をクリックするとFree threading関係のオプションを含めたカスタムインストールのオプションが表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/133233/6b55af99-16f8-47e2-e1b4-49c806099a2c.png)

下記の "Free-threaded Python [experimental]" にチェックを入れ「インストール」ボタンを押すと、Free threading対応のPython処理系がインストールされます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/133233/5ea41861-4aaa-5a1f-5718-8076f3b8bfa7.png)


### 方法２：CUIインストーラ

GUI版と同様にインストーラをダウンロードした後、[py-free-threadingのインストール方法](https://py-free-threading.github.io/installing_cpython/#__tabbed_1_2)に従います。

```console
$ cat > ./choicechanges.plist <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<array>
        <dict>
                <key>attributeSetting</key>
                <integer>1</integer>
                <key>choiceAttribute</key>
                <string>selected</string>
                <key>choiceIdentifier</key>
                <string>org.python.Python.PythonTFramework-3.13</string>
        </dict>
</array>
</plist>
EOF
$ sudo installer -pkg ./python-3.13.0rc3-macos11.pkg \
  -applyChoiceChangesXML ./choicechanges.plist \
  -target /
```

オプションの設定は[CPythonのGitHub Issue](https://github.com/python/cpython/issues/120098#issuecomment-2151122033)に書かれているもののようです。

### 動作確認

上記の方法でインストールした場合、 `/usr/local/bin` に `python` バイナリがインストールされます。 `python3.13t` のようにファイル名の末尾に `t` がついたものがFree threading対応のPythonインタプリタです。

下記のように `-VV` オプションをつけて `python3.13t` を実行すると、 `experimental free-threading build` のようにFree threadingが有効化されていることが確認できます。

```console
$ /usr/local/bin/python3.13t -VV
Python 3.13.0rc3 experimental free-threading build (v3.13.0rc3:fae84c70fbd, Oct  1 2024, 00:22:06) [Clang 15.0.0 (clang-1500.3.9.4)]
```

また、 `sys._is_gil_enabled()` でGILが無効になっていることを確認できます。

```console
$ /usr/local/bin/python3.13t -c "import sys; print(sys._is_gil_enabled())"

False
```

## pytest-freethreaded

Sprintでは、リーダーの [tonybaloney](https://github.com/tonybaloney) さんが企画したFree threading関係のプロジェクトに参加しました。

このプロジェクトは2つのタスクがあり、そのうちの一つがpytestのプラグイン [pytest-freethreaded](https://github.com/tonybaloney/pytest-freethreaded) の開発と検証でした。このプラグインは、複数のスレッドで同時にテストケースを実行することで、Free Threading環境でのライブラリの動作を検証することができます。

[tonybaloney](https://github.com/tonybaloney) さんを中心にプラグインはすごい速さでプロトタイピングされ、午後の早いうちに動作するようになっていました。

私がFree threadingのドキュメントを読んでサンプルコードを書いている間にプラグインができていた、という感じでした。[tonybaloney](https://github.com/tonybaloney) さん、[m-clare](https://github.com/m-clare) さんと相談の上、pytest-freethreadedを用いたOSSライブラリの検証を担当することになりました。

その経験をもとに、プラグインのインストール方法と使い方を説明します。

### プラグインのインストール

まず、`python3.13t` の仮想環境を作リます。

```console
$ /usr/local/bin/python3.13t -m venv .venv
$ source .venv/bin/activate
```

このプラグインは執筆時点(2024/10/01)でまだPyPIで公開されていません。
そのため、[GitHubリポジトリ](https://github.com/tonybaloney/pytest-freethreaded)からインストールします。

```console
$ pip install git+https://github.com/tonybaloney/pytest-freethreaded.git
```

(参考) もし、pytest-freethreaded自体の開発をする場合には、[README](https://github.com/tonybaloney/pytest-freethreaded?tab=readme-ov-file#development)にあるようにeditable installします

```console
$ git clone git@github.com:tonybaloney/pytest-freethreaded.git
$ pip install flit
$ flit install -s
```


### 使い方

通常の `pytest` と同様に `pytest` コマンドを実行すると `pytest-freethreaded`
が各テストケースを並列に反復して実行します。

執筆時点でのデフォルトの並列数は10、反復数は200です([code](https://github.com/tonybaloney/pytest-freethreaded/blob/16b2865853342fd0d65feae697572a170eaf2cb0/src/pytest_freethreaded/plugin.py#L12-L26))。

```console
$ pytest -vvvv --log-level=DEBUG
```

反復数だけテストの実行時間も増えます。
実際の運用ではデフォルトはなにもしないようにしておいて（並列数を1、反復数も1）、気になるテストケースだけプラグインを有効にする、という使い方が現実的かもしれません ([example](https://github.com/tonybaloney/pytest-freethreaded/blob/16b2865853342fd0d65feae697572a170eaf2cb0/test_pytest_racy_fixture.py#L36-L39))。

```python
@pytest.mark.freethreaded(threads=20, iterations=200)
def test_foo(...):
    ...
````

## pytest-freethreaded を用いたOSSのテストの例

ここでは、OSSのテストの例としてPillowのテストを並列に実行してみます。
こちらは [m-clare](https://github.com/m-clare) さんがSprint中にSegmentation faultを発見されており、その再現を試みます。

https://github.com/tonybaloney/pytest-freethreaded/issues/9

### Pillowのインストール

初めに、Pillowのリポジトリをクローンします。

```console
$ git clone git@github.com:python-pillow/Pillow.git
$ cd Pillow 
```

次に、Pillowをソースからビルドしてインストールします。
これは、各ライブラリでFree threading対応が日進月歩で進んでおり、最新版で検証をするためです。

執筆時点でのコミットは下記です。お手元で再現される際には問題が修正されている可能性があります。
https://github.com/python-pillow/Pillow/commit/84b167dfd53fe726a61953040ddbfa0e4e3e3359

ソースからのインストールは各ライブラリの説明に従ってください。Pillowの場合は下記です：

https://pillow.readthedocs.io/en/latest/installation/building-from-source.html

macOSの場合には、下記の依存ライブラリのインストールが必要でした。

```console
$ brew install libjpeg libtiff little-cms2 openjpeg webp
$ brew install freetype harfbuzz fribidi
```

依存ライブラリをインストールした後は、通常通り pip でインストール可能です。

```console
$ pip install .
```

### pytestの実行

問題発生時に、詳細な情報が出力されることを期待して、以下のようにオプションを足してpytestを実行します。

```console
$ pytest -vvvv --log-level=DEBUG
```

下記のように、Segmentation Faultが再現されました。

```
Tests/oss-fuzz/test_fuzzers.py::test_fuzz_fonts[Tests/fonts/DejaVuSans/DejaVuSans-24-4-stripped.ttf] PASSED                                 [ 22%]
Tests/oss-fuzz/test_fuzzers.py::test_fuzz_fonts[Tests/fonts/DejaVuSans/DejaVuSans-24-2-stripped.ttf] Fatal Python error: Segmentation fault

Current thread 0x0000000177a8f000 (most recent call first):
  File "/Users/yanase/code/pyconjp-sprint-2024/ext/Pillow/Tests/oss-fuzz/test_fuzzers.py", line 64 in test_fuzz_fonts
  File "/Users/yanase/code/pyconjp-sprint-2024/ext/Pillow/.venv/lib/python3.13t/site-packages/_pytest/python.py", line 159 in pytest_pyfunc_call
  File "/Users/yanase/code/pyconjp-sprint-2024/ext/Pillow/.venv/lib/python3.13t/site-packages/pluggy/_callers.py", line 103 in _multicall
  File "/Users/yanase/code/pyconjp-sprint-2024/ext/Pillow/.venv/lib/python3.13t/site-packages/pluggy/_manager.py", line 120 in _hookexec
  File "/Users/yanase/code/pyconjp-sprint-2024/ext/Pillow/.venv/lib/python3.13t/site-packages/pluggy/_hooks.py", line 513 in __call__
  File "/Users/yanase/code/pyconjp-sprint-2024/ext/Pillow/.venv/lib/python3.13t/site-packages/_pytest/python.py", line 1627 in runtest
  File "/Users/yanase/code/pyconjp-sprint-2024/ext/Pillow/.venv/lib/python3.13t/site-packages/pytest_freethreaded/plugin.py", line 63 in get_one_result
  File "/Library/Frameworks/PythonT.framework/Versions/3.13/lib/python3.13t/concurrent/futures/thread.py", line 58 in run
  File "/Library/Frameworks/PythonT.framework/Versions/3.13/lib/python3.13t/concurrent/futures/thread.py", line 92 in _worker
  File "/Library/Frameworks/PythonT.framework/Versions/3.13/lib/python3.13t/threading.py", line 992 in run
  File "/Library/Frameworks/PythonT.framework/Versions/3.13/lib/python3.13t/threading.py", line 1041 in _bootstrap_inner
  File "/Library/Frameworks/PythonT.framework/Versions/3.13/lib/python3.13t/threading.py", line 1012 in _bootstrap

Thread 0x0000000176a83000 (most recent call first):
  File "/Users/yanase/code/pyconjp-sprint-2024/ext/Pillow/Tests/oss-fuzz/test_fuzzers.py", line 64 in test_fuzz_fonts
  File "/Users/yanase/code/pyconjp-sprint-2024/ext/Pillow/.venv/lib/python3.13t/site-packages/_pytest/python.py", line 159 in pytest_pyfunc_call
  File "/Users/yanase/code/pyconjp-sprint-2024/ext/Pillow/.venv/lib/python3.13t/site-packages/pluggy/_callers.py", line 103 in _multicall
  File "/Users/yanase/code/pyconjp-sprint-2024/ext/Pillow/.venv/lib/python3.13t/site-packages/pluggy/_manager.py", line 120zsh: segmentation fault  .venv/bin/pytest -vvvv --log-level=DEBUG
```

このプラグインは、現状すべてのテストケースを並列化して反復実行していますが、テストケースによっては並列実行を想定していない場合もありそうです。検証対象のライブラリにUpstream活動をする際には、ライブラリを修正すべきなのか、テストに問題があるのか、それとも、そもそもこのプラグインを使うべきではないテストなのか、などについて事前に考えてみるとよりよいコントリビューションができるかもしれません。

また、私はSprint中には scikit-image の検証をしていたのですが、まず scikit-image のインストールをする際のエラー解消に苦労しました。下記のIssueで報告していますので、もしご興味があれば再現に取り組んでみていただけると幸いです。

https://github.com/tonybaloney/pytest-freethreaded/issues/12


## 最後に

PyCon JP Sprintに参加するまでは「GILが無効化できるとマルチスレッディングが速くなってうれしいな」と能天気に考えていましたが、 [pytest-freethreaded](https://github.com/tonybaloney/pytest-freethreaded) のプロジェクトに参加してみて、意外と影響の大きな変更であることを実感しました。

また、リーダーの [tonybaloney](https://github.com/tonybaloney) さんをはじめ、メンバーの皆様がすごい勢いで開発、検証される姿に刺激を受けました。私にとって、とても良い開発体験でした。

Free Threadingの普及に備え、今後もpytest-freethreadedへの貢献やライブラリの対応状況の検証を続けていきたいと思います。

## 謝辞

PyCon JP 2024 Sprintの運営の皆様、リーダーの皆様、参加者の皆様に感謝します。素晴らしい機会をありがとうございました。

## 参考文献

- [pytest-freethreaded](https://github.com/tonybaloney/pytest-freethreaded)
- [py-free-threading](https://py-free-threading.github.io/)
- [PEP 703](https://peps.python.org/pep-0703/)
- [C API Extension Support for Free Threading](https://docs.python.org/3.13/howto/free-threading-extensions.html)
- [Python 3.13のFree-Threadingを試してみた](https://zenn.dev/yasudakn/articles/7020cfa554e5a9)