Skip to content

Make lock behavior controllable, add xxhash.threadsafe submodule, and clean up#162

Open
ifduyue wants to merge 21 commits into
masterfrom
no-lock
Open

Make lock behavior controllable, add xxhash.threadsafe submodule, and clean up#162
ifduyue wants to merge 21 commits into
masterfrom
no-lock

Conversation

@ifduyue

@ifduyue ifduyue commented Jun 20, 2026

Copy link
Copy Markdown
Owner
  • Remove deprecated VERSION_TUPLE and bump version to 4.0.0.dev0
  • Make _xxhash.c lock behaviour controllable via XXHASH_WITH_LOCK and support a parameterized module name
  • Build both _xxhash and _xxhash_threadsafe extensions; default is locked on free-threading builds
  • Add xxhash.threadsafe submodule backed by the locked _xxhash_threadsafe extension
  • Update thread-safety tests to use xxhash.threadsafe
  • Document thread-safety model and the new xxhash.threadsafe submodule
  • Unify behaviour across GIL and free-threading builds: default module is always unlocked
  • Declare both extension variants as Py_MOD_GIL_NOT_USED on Python 3.13+
  • Use set literal syntax instead of set([...])
  • Remove empty if (!kwargs) branch in _parse_init_args
  • Fix typo: synchronisation -> synchronization
  • Update XXHASH_DO_UPDATE comment to describe both locked and unlocked paths
  • Document why build_ext restores self.build_temp in try/finally
  • Macro-generate the 4 update() methods via XXHASH_UPDATE_METHOD
  • Align trailing backslashes in multi-line macros
  • Remove Py_ALWAYS_INLINE

@ifduyue ifduyue changed the title no lock Make lock behavior controllable, add xxhash.threadsafe submodule, and clean up Jun 20, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request optimizes performance by making the default streaming hash objects in xxhash non-thread-safe by default, removing per-object locking overhead. A new xxhash.threadsafe submodule is introduced to provide thread-safe streaming hash objects with per-object locks. The C extension src/_xxhash.c is refactored to support compiling both variants (_xxhash and _xxhash_threadsafe) from the same source, and setup.py is updated to build both extensions. Additionally, the deprecated VERSION_TUPLE is removed, and the package version is bumped to 4.0.0.dev0. There are no review comments, so I have no feedback to provide.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

@codspeed-hq

codspeed-hq Bot commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 148 untouched benchmarks
⏩ 66 skipped benchmarks1


Comparing no-lock (f4dee84) with master (c5fde1c)

Open in CodSpeed

Footnotes

  1. 66 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

ifduyue added 4 commits June 22, 2026 15:48
…eclarations

The stub file xxhash/__init__.pyi incorrectly defined xxh64_digest,
xxh64_hexdigest, xxh64_intdigest as aliases of xxh3_64_* functions.
xxh64 and xxh3_64 are separate algorithms with potentially different
hash outputs, so they should have independent function declarations.

This aligns with the already-correct threadsafe.pyi.
The previous lazy allocation in XXHASH_LOCK_MAYBE_INIT only created a
PyThread_type_lock if update() was called with data >= 64KB. This meant
that if a thread-safe hash object was only used with small data (or never
called update()), digest(), copy(), reset(), etc. all ran without any
synchronization because XXHASH_LOCK_ACQUIRE was a no-op when lock == NULL.

Fix: always allocate the lock at initialization via XXHASH_LOCK_INIT,
matching hashlib's approach of always-initialized mutex (HASHLIB_INIT_MUTEX).
This eliminates the security window entirely.

Changes:
- XXHASH_LOCK_INIT now calls PyThread_allocate_lock() instead of setting NULL
- XXHASH_LOCK_MAYBE_INIT reduced to a no-op (lock always allocated)
- Simplified _do_update by removing the lazy init call and dead 'no lock'
  branch - locking is now unconditional (no-op in non-lock build)
- NULL guards in acquire/release/fini retained as defensive safety

Python 3.13+ (PyMutex path) was not affected as it was already always-on.
…RUCTIONS_LOCKED

Two changes:

1. Simplify lock macros to match 3.15's unconditional acquire/release:
   - PyMutex path (3.13+): retain only FIELD/INIT/FINI/ACQUIRE/RELEASE;
     remove IS_ACTIVE, MAYBE_INIT, ACQUIRE_BLOCKING (no longer needed)
   - PyThread_type_lock path (3.9-3.12): ACQUIRE/RELEASE are now
     unconditional blocking calls (no NULL guards, no try-then-block),
     matching 3.15's HASHLIB_ACQUIRE_LOCK
   - Non-lock path: keep only mandatory macros

2. Simplify _do_update to 3.15's pattern:
   - Both large and small data paths use the SAME XXHASH_LOCK_ACQUIRE
   - Size threshold (XXHASH_GIL_MINSIZE) only controls GIL release,
     NOT whether locking happens
   - Matches 3.15's HASHLIB_EXTERNAL_INSTRUCTIONS_LOCKED exactly
…k under GIL

4443f28 incorrectly simplified the 3.9-3.12 PyThread_type_lock path to
unconditional WAIT_LOCK. This is unsafe under GIL:

  PyThread_acquire_lock(lock, WAIT_LOCK) does NOT release the GIL
  while waiting, unlike PyMutex_Lock (3.13+) which passes _PY_LOCK_DETACH
  and releases GIL when parking.

Deadlock scenario:
  1. T1 holds object lock (large data path, GIL released)
  2. T2 holds GIL, enters small data path, calls WAIT_LOCK → blocks with GIL
  3. T1 finishes hash, tries to reacquire GIL → blocked
  4. T2 waits for object lock, T1 waits for GIL → deadlock

Fix: restore the try-then-block in XXHASH_LOCK_ACQUIRE (used under GIL):
  - First try NOWAIT_LOCK (fast path)
  - If contested, release GIL (Py_BEGIN_ALLOW_THREADS), then WAIT_LOCK
  - Reacquire GIL (Py_END_ALLOW_THREADS)

XXHASH_LOCK_ACQUIRE_BLOCKING (used without GIL, large data path) keeps
simple WAIT_LOCK since GIL is already released.

3.13+ PyMutex path remains unconditional (safe due to _PY_LOCK_DETACH).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant