Skip to content

Conversation

@ngoldbaum
Copy link

@ngoldbaum ngoldbaum commented Sep 8, 2025

Closes #467

This adds support for Python 3.14 and 3.14t as well as supporting free-threaded Python.

I've opted to add a threading.RLock member to the SoundFile object. I also made it so all the public methods of SoundFile that access the _file handle need to acquire the lock via a new with_lock decorator.

I'm using an RLock to avoid the need to refactor to avoid possibly reentrancy. I can probably use a threading.Lock instead but it will be trickier to get right.

Also adds tests for supported workflows (each thread has its own SoundFile) as well as unsupported workflows.

You could also support the free-threaded build without adding the locking. IMO the locking is a nicer user experience. With this approach if you try to use a SoundFile object in more than one thread simultaneously, you will see a RuntimeError if there is any concurrent use. Since I'm using the non-blocking variant of RLock.acquire(), it's impossible for this to introduce new scaling issues. Acquiring an uncontended lock is very low-overhead so I don't expect this to add much overhead.

I ran the tests under TSan and didn't see any issues besides a known race in BytesIO in 3.14 that should be fixed in 3.14.1. I can add TSan CI as a followup if you'd like.

@ngoldbaum
Copy link
Author

@bastibe gentle ping on this. If you'd like any help on my end with other maintenance tasks on the repo I'm also happy to do that, especially if that makes you more likely to help me ship this!

@bastibe
Copy link
Owner

bastibe commented Oct 15, 2025

I'm sorry for not being able to work on this at the moment. Fall is a very busy time for me. I hope I'll be able to have a look on the weekend.

@bastibe
Copy link
Owner

bastibe commented Jan 10, 2026

Sorry for not responding for so long. It was indeed an incredibly busy time for me.

At any rate, I've had a look now. This pull request adds a lock to each soundfile object, thereby preventing any concurrent use. This prevents crashes if you write code that accesses the same soundfile object from multiple threads.

However, I am a bit hesitant about this, as it doesn't fix concurrency, but merely prevents it. It's certainly a better user experience to throw an exception instead of crashing with a segfault, but I'm not sure if this is actually helping.

I've not followed the multi-threaded python development particularly closely. What is the common pattern for dealing with non-thread-safe code? Do we expect all non-thread-safe code to secure itself?

@ngoldbaum
Copy link
Author

ngoldbaum commented Jan 15, 2026

However, I am a bit hesitant about this, as it doesn't fix concurrency, but merely prevents it. It's certainly a better user experience to throw an exception instead of crashing with a segfault, but I'm not sure if this is actually helping.

I've not followed the multi-threaded python development particularly closely. What is the common pattern for dealing with non-thread-safe code? Do we expect all non-thread-safe code to secure itself?

Not necessarily - another option is to document the thread safety issues and leave it up to users to handle it, including any crashes that might happen.

We've written some documentation on multithreaded Python programming patterns here: https://py-free-threading.github.io/porting/#multithreaded-python-programming

There's also some documentation here on similar patterns for more low-level code: https://py-free-threading.github.io/porting-extensions/#dealing-with-thread-unsafe-native-libraries

Soundfile uses cffi, so it's a combination of the two patterns: interacting with a low-level library directly from Python.

Another way to handle this would be to lock the mutex using a blocking lock and allow concurrent uses of a shared SoundFile.

For that case - do you have a workflow in mind where threads would concurrently work with the same SoundFile object? I could then write a test that models this workflow.

To assuage concerns about the approach used in this PR: it's also always fine to change an exception into something else in the future, so if you are able to figure out a better concurrency story, you could always remove the locking and exceptions.

This approach also doesn't change the status quo in that anyone trying to use python-soundfile in a multithreaded program (free-threaded build or no) will see crashes that this PR turns into runtime errors. So anyone using this library with threads is already dealing with this somehow, e.g. by not sharing the SoundFIile objects between threads or with some other external synchronization approach.

with:
python-version: ${{ matrix.python-version }}
architecture: ${{ matrix.architecture }}
allow-prereleases: true
Copy link
Author

Choose a reason for hiding this comment

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

This isn't necessary now that Python3.14 final is out.

Suggested change
allow-prereleases: true

"cffi>=2.0.0b1; python_version >= '3.14'",
'numpy',
'typing-extensions'
],
Copy link
Author

Choose a reason for hiding this comment

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

I think I can revert all this now that there's a final CFFI release out. Python 3.14 can only install CFFI 2.0 or newer.

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.

Support the Python 3.14t free-threaded build

2 participants