The cache had worked for years.
Check if the key is there.
If it is not,
build the value
and put it in.
A dozen worker threads
shared it,
and nobody ever saw
two threads build the same thing.
Nobody wrote a lock.
Nobody needed one,
or so it seemed.
Then someone tried
the free threaded build of Python
to get the speed up
everyone was talking about.
The speed up was real.
So were the duplicates,
the half built entries,
and a counter
that no longer added up.
Here is the mechanism.
For thirty years
CPython had a global interpreter lock.
One thread ran Python code
at a time.
It was never a promise
that your code was safe.
Check then act
was always a race.
But with only one thread moving,
the windows were so small
that the race almost never happened,
and almost never
started to feel like never.
PEP 703 removes the lock.
Python 3.13 shipped
an experimental build without it.
In 3.14
that build became officially supported.
It is still optional,
not the default,
and the direction is clear.
Without the lock,
your threads really do run
at the same moment
on different cores.
The built in types
protect themselves,
so a dict will not be corrupted.
Your logic will not.
The gap between your check
and your act
is now as wide
as real parallel hardware.
There is a quieter trap too.
On the free threaded build,
importing a C extension
that has not declared it is safe
switches the lock back on,
with a warning
most logs will swallow.
You can be running the new build
and still be running the old behaviour,
and believe you have tested the new one.
So start now,
while it is optional.
Find every check then act
on shared state,
and put a lock around it,
or give each thread its own copy.
Run your tests
on the free threaded build
in CI,
and assert in code
whether the lock is really off.
Ask every native dependency
whether it supports it yet.
A lock you never wrote
because a bigger one was hiding
is not a design.
It is a habit
the runtime is about to break.
– Serguey Asael Shinder
Top comments (0)