Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
a21cf27
Remove deprecated VERSION_TUPLE and bump version to 4.0.0.dev0
ifduyue Jun 20, 2026
1c5197d
Make _xxhash.c lock behaviour controllable via XXHASH_WITH_LOCK and s…
ifduyue Jun 20, 2026
501a187
Build both _xxhash and _xxhash_threadsafe extensions; default is lock…
ifduyue Jun 20, 2026
be0ed4b
Add xxhash.threadsafe submodule backed by the locked _xxhash_threadsa…
ifduyue Jun 20, 2026
92f6668
Update thread-safety tests to use xxhash.threadsafe
ifduyue Jun 20, 2026
6f7fbb7
Document thread-safety model and the new xxhash.threadsafe submodule
ifduyue Jun 20, 2026
bc5dfb4
Unify behaviour across GIL and free-threading builds: default module …
ifduyue Jun 20, 2026
611861c
Declare both extension variants as Py_MOD_GIL_NOT_USED on Python 3.13+
ifduyue Jun 20, 2026
cc3c3a1
Use set literal syntax instead of set([...])
ifduyue Jun 20, 2026
d0e36cd
Remove empty if (!kwargs) branch in _parse_init_args
ifduyue Jun 20, 2026
645b813
Fix typo: synchronisation -> synchronization
ifduyue Jun 20, 2026
b94fa48
Update XXHASH_DO_UPDATE comment to describe both locked and unlocked …
ifduyue Jun 20, 2026
3a11027
Document why build_ext restores self.build_temp in try/finally
ifduyue Jun 20, 2026
e3782ca
Macro-generate the 4 update() methods via XXHASH_UPDATE_METHOD
ifduyue Jun 20, 2026
39bfe1f
Align trailing backslashes in multi-line macros
ifduyue Jun 20, 2026
9322cf5
Remove Py_ALWAYS_INLINE
ifduyue Jun 20, 2026
e960610
Discourage concurrent update/reset even with xxhash.threadsafe
ifduyue Jun 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ NEXT
~~~~~~~~~~~~~~~~~

- Drop support for Python 3.8
- Remove deprecated ``xxhash.VERSION_TUPLE``
- Default streaming hash objects (``xxh32``, ``xxh64``, ``xxh3_64``,
``xxh3_128``) are no longer thread-safe by default; this removes
per-object locking overhead and restores performance as the primary goal
- Add ``xxhash.threadsafe`` submodule for users who need to share a
streaming hash object across threads; it provides the same API with a
per-object lock
- Both the default module and ``xxhash.threadsafe`` are provided on
free-threading (no-GIL) Python builds, matching the API on regular GIL
builds

v3.7.0 2025-04-25
~~~~~~~~~~~~~~~~~
Expand Down
30 changes: 30 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,36 @@ And aliases:
| xxh128_intdigest = xxh3_128_intdigest
| xxh128_hexdigest = xxh3_128_hexdigest

Thread safety
-------------

The default ``xxhash`` module is optimized for speed. Streaming hash objects
(``xxh32``, ``xxh64``, ``xxh3_64``, ``xxh3_128`` / ``xxh128``) are **not**
thread-safe: do not call ``update()``, ``digest()``, ``copy()``, ``reset()``,
or any other mutating method on the same object from multiple threads without
external synchronization.

One-shot functions (``xxh32_digest``, ``xxh64_hexdigest``, ``xxh3_128_digest``,
etc.) are stateless and always safe to call concurrently.

Concurrent ``update()`` / ``reset()`` on a shared streaming hash object is
discouraged even with locking — prefer one-shot functions or per-thread hash
objects. If you must share a streaming hash across threads, use the
``xxhash.threadsafe`` submodule. It provides the same API with a per-object
lock that serializes all access to the internal xxHash state:

.. code-block:: python

>>> from xxhash import threadsafe
>>> h = threadsafe.xxh64()
>>> # h can be updated from multiple threads, but concurrent update/reset
>>> # still adds overhead and is not recommended

The same two-module split is provided on free-threading (no-GIL) Python
builds: the default module is unlocked, and ``xxhash.threadsafe`` provides a
locked variant.


Caveats
-------

Expand Down
46 changes: 42 additions & 4 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from pathlib import Path

from setuptools import Extension, setup
from setuptools.command.build_ext import build_ext as _build_ext

if os.getenv("XXHASH_LINK_SO"):
libraries = ["xxhash"]
Expand All @@ -12,15 +13,51 @@
source = ["src/_xxhash.c", "deps/xxhash/xxhash.c"]
include_dirs = ["deps/xxhash"]

# The default ``xxhash._xxhash`` extension is built without per-object locks
# for maximum performance. Users who need to share a streaming hash object
# across threads can use ``xxhash._xxhash_threadsafe`` (exposed as the public
# ``xxhash.threadsafe`` submodule), which is compiled from the same source
# with locking enabled.

_ext_kwargs = {
"sources": source,
"include_dirs": include_dirs,
"libraries": libraries,
}

ext_modules = [
Extension(
"_xxhash",
source,
include_dirs=include_dirs,
libraries=libraries,
)
**_ext_kwargs,
),
Extension(
"_xxhash_threadsafe",
define_macros=[("XXHASH_WITH_LOCK", "1"), ("XXHASH_MODULE_NAME", "_xxhash_threadsafe")],
**_ext_kwargs,
),
]


class build_ext(_build_ext):
"""Build each extension in its own temp directory.

Both extensions are built from the same ``src/_xxhash.c`` source file.
Without separate temp directories their object files would overwrite
each other, causing one variant to be linked with the wrong macros.

``try/finally`` restores ``self.build_temp`` so that incremental builds
(where ``build_ext`` may be reused) still work correctly.
"""

def build_extension(self, ext):
old_build_temp = self.build_temp
self.build_temp = os.path.join(old_build_temp, ext.name)
try:
super().build_extension(ext)
finally:
self.build_temp = old_build_temp


d = Path(__file__).parent
long_description = d.joinpath("README.rst").read_text() + "\n" + d.joinpath("CHANGELOG.rst").read_text()

Expand Down Expand Up @@ -58,5 +95,6 @@
],
python_requires=">=3.9",
ext_modules=ext_modules,
cmdclass={"build_ext": build_ext},
package_data={"xxhash": ["py.typed", "**.pyi"]},
)
Loading
Loading