Skip to content

Commit 9feeaca

Browse files
authored
Trying out free-threaded Python on macOS
1 parent 6cf4849 commit 9feeaca

1 file changed

Lines changed: 114 additions & 0 deletions

File tree

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
# Trying out free-threaded Python on macOS
2+
3+
Inspired by [py-free-threading.github.io](https://py-free-threading.github.io/) I decided to try out a beta of Python 3.13 with the new free-threaded mode enabled, which removes the GIL.
4+
5+
## Installation
6+
7+
I chose to use the macOS installer to get a pre-built binary. I downloaded and ran the macOS installer from [www.python.org/downloads/release/python-3130b3/](https://www.python.org/downloads/release/python-3130b3/), and then when I got to this screen:
8+
9+
![Installation dialog with a Customize button at the bottom](https://github.com/user-attachments/assets/7e57d8f1-6a4b-4551-babd-127317dff5cd)
10+
11+
I selected the "Customize" option and checked this additional box:
12+
13+
![Customize screen - I have checked the Free-threaded Python (experimental) option](https://github.com/user-attachments/assets/5aa8d4dd-5c70-493e-a183-2f0799079830)
14+
15+
Once it had finished installing I didn't run the script to add it to my path (I didn't want to intefere with my many other Python versions).
16+
17+
This gave me two new Python binaries - one with free-threading enabled and one without:
18+
19+
- `/Library/Frameworks/Python.framework/Versions/3.13/bin/python3.13`
20+
- `/Library/Frameworks/PythonT.framework/Versions/3.13/bin/python3.13t`
21+
22+
Note the `PythonT.framework` in the second path.
23+
24+
It also seemed to setup the following aliases in `/usr/local/bin`:
25+
26+
- `/usr/local/bin/python3.13`
27+
- `/usr/local/bin/python3.13t`
28+
29+
These are symlinks:
30+
```bash
31+
ls -lah /usr/local/bin | grep python3.13
32+
```
33+
```
34+
lrwxr-xr-x 1 root wheel 73B Jul 12 16:26 python3.13 -> ../../../Library/Frameworks/Python.framework/Versions/3.13/bin/python3.13
35+
lrwxr-xr-x 1 root wheel 80B Jul 12 16:26 python3.13-config -> ../../../Library/Frameworks/Python.framework/Versions/3.13/bin/python3.13-config
36+
lrwxr-xr-x 1 root wheel 81B Jul 12 16:26 python3.13-intel64 -> ../../../Library/Frameworks/Python.framework/Versions/3.13/bin/python3.13-intel64
37+
lrwxr-xr-x 1 root wheel 75B Jul 12 16:26 python3.13t -> ../../../Library/Frameworks/PythonT.framework/Versions/3.13/bin/python3.13t
38+
lrwxr-xr-x 1 root wheel 82B Jul 12 16:26 python3.13t-config -> ../../../Library/Frameworks/PythonT.framework/Versions/3.13/bin/python3.13t-config
39+
```
40+
41+
Starting those Python processes shows which one has free-threading in the interpreter header:
42+
```
43+
% /usr/local/bin/python3.13
44+
Python 3.13.0b3 (v3.13.0b3:7b413952e8, Jun 27 2024, 09:57:31) [Clang 15.0.0 (clang-1500.3.9.4)] on darwin
45+
Type "help", "copyright", "credits" or "license" for more information.
46+
>>>
47+
% /usr/local/bin/python3.13t
48+
Python 3.13.0b3 experimental free-threading build (v3.13.0b3:7b413952e8, Jun 27 2024, 10:04:51) [Clang 15.0.0 (clang-1500.3.9.4)] on darwin
49+
Type "help", "copyright", "credits" or "license" for more information.
50+
>>>
51+
```
52+
53+
## Testing it out
54+
55+
I asked Claude 3.5 Sonnet to write me [a quick test script](https://claude.site/artifacts/92aec50b-43c7-463a-b8a1-5d5ea0600708), then iterated on and simplified the result myself until I got to this:
56+
57+
```python
58+
import argparse
59+
import time
60+
from concurrent.futures import ThreadPoolExecutor
61+
62+
63+
def cpu_bound_task(n):
64+
"""A CPU-bound task that computes the sum of squares up to n."""
65+
return sum(i * i for i in range(n))
66+
67+
68+
def main():
69+
parser = argparse.ArgumentParser(description="Run a CPU-bound task with threads")
70+
parser.add_argument("--threads", type=int, default=4, help="Number of threads")
71+
parser.add_argument("--tasks", type=int, default=10, help="Number of tasks")
72+
parser.add_argument(
73+
"--size", type=int, default=5000000, help="Task size (n for sum of squares)"
74+
)
75+
args = parser.parse_args()
76+
77+
print(f"Running {args.tasks} tasks of size {args.size} with {args.threads} threads")
78+
79+
start_time = time.time()
80+
with ThreadPoolExecutor(max_workers=args.threads) as executor:
81+
list(executor.map(cpu_bound_task, [args.tasks] * args.size))
82+
end_time = time.time()
83+
duration = end_time - start_time
84+
85+
print(f"Time with threads: {duration:.2f} seconds")
86+
87+
88+
if __name__ == "__main__":
89+
main()
90+
```
91+
I saved this as `gildemo.py` and tried running it with the new Python binaries the one with free-threading enabled and the one without.
92+
93+
Here's what I saw in Activity Monitor while running the scripts:
94+
95+
No free-threading (with the GIL) - reported 99% CPU usage:
96+
97+
![Activity Monitor window showing a Python process using 99.2% CPU with 5 threads. Terminal visible above running a script: "Running 10 tasks of size 10000000 with 4 threads"](https://github.com/user-attachments/assets/686e2dcd-daff-4cfe-ba1e-c4aa38b0a10a)
98+
99+
Free-threading (no GIL) - reported 258.8% CPU usage:
100+
101+
![Activity Monitor window showing a PythonT process using 258.8% CPU with 5 threads. Terminal visible above running a script: "Running 10 tasks of size 10000000 with 4 threads".](https://github.com/user-attachments/assets/e7b8d291-a8a1-4e98-a73f-880403e14e8c)
102+
103+
And here are some results from running the script. First with the GIL in place:
104+
```
105+
% python3.13 gildemo.py --size 1000000
106+
Running 10 tasks of size 1000000 with 4 threads
107+
Time with threads: 17.14 seconds
108+
```
109+
And then without the GIL:
110+
```
111+
% python3.13t gildemo.py --size 1000000
112+
Running 10 tasks of size 1000000 with 4 threads
113+
Time with threads: 8.19 seconds
114+
```

0 commit comments

Comments
 (0)